
现在免费的杀毒软件越来越多了,例如360安全卫士、金山毒霸、瑞星……为什么还会存在专杀工具呢?猜测有3个原因,第1个原因是虽然有很多免费杀毒软件,但还是有很多人不安装杀毒软件,如果爆发了传播速度较快、感染规模较大的病毒或蠕虫的话,杀毒厂商会为了快速阻止这种比较“暴力”的病毒或蠕虫的传播与感染,而推出轻量级的供网友使用的工具;第2个原因是杀毒厂商的杀毒软件对于某种病毒无能为力,为了能尽快挽回自己的杀毒软件无能的颜面,而推出的一种方案;第3个原因是感染病毒的人为了解决自己的问题而编写的。
专杀工具是针对某一个或某一类的病毒、木马或蠕虫等恶意软件而开发的工具。专业的杀毒软件需要专业的反病毒公司来进行开发;而专杀工具可能是由反病毒公司开发的,也可能是由个人进行开发的。本文介绍如何开发专杀工具。
1. 病毒的分析方法
病毒的分析方法一般有两种,分别是行为分析和逆向分析。个人编写专杀工具,一般针对的都是非感染型的病毒,当然也有针对感染型的病毒的,但是后者相对比较少一些。对于非感染型的病毒,通常情况下并不需要对病毒做逆向分析,只需要对病毒进行行为分析就可以编写专杀工具。而如果病毒是感染型的,为了能够修复被病毒感染的文件,那么就不能只是简单地对病毒进行行为分析,必须对病毒进行逆向分析,从而进一步修复被病毒感染或破坏的文件。下面分别介绍什么是行为分析,什么是逆向分析。
病毒、木马等恶意程序都有一些比较隐蔽的“小动作”,而这些动作一般情况下是正常程序所没有的。比如,把自己添加进启动项,或把自己的某个DLL文件注入其他进程中,或把自己复制到系统目录下……这些行为一般都不是应用软件该有的正常行为。用户拿到一个病毒样本以后,通常是将病毒复制到虚拟机中,然后打开一系列监控工具,比如注册表监控、文件监控、进程监控、网络监控等,将各种准备工作做好以后,在虚拟机中把病毒运行起来,看病毒对注册表进行了哪些操作,对文件进行了哪些操作,连接了哪个IP地址、创建了多少进程。通过观察这一系列操作,就可以写一个程序,只要把它创建的进程结束,把它写入注册表的内容删除,把它新建的文件删除,就等于把这个病毒杀掉了。当然,整个过程并不会像说起来这么容易。通过一系列系统监控工具找出病毒的行为就是行为分析方法。
当病毒感染可执行文件以后,感染的是什么内容是无法通过行为监控工具发现的。而病毒对可执行文件的感染,有可能是添加一个新节来存放病毒代码,也可能是通过节与节之间的缝隙来存放病毒代码的。无论是哪种方式,都需要通过逆向的手段进行分析。通过逆向分析的方法分析病毒,就称为逆向分析法,也有人称其为高级分析,因为掌握逆向分析的能力要比掌握行为分析有难度。逆向分析的工具通常有OD、IDA、WinDBG等。
2. 病毒查杀方法简介
病毒的查杀方法有很多种。在网络安全日益普及、杀毒软件公司大力宣传的今天,想必大部分关心网络安全的人对于病毒查杀的技术有了一些了解。当今常见的主流病毒查杀技术有特征码查杀、启发式查杀、虚拟机查杀和主动防御等。下面简单地介绍特征码查杀、启发式查杀和虚拟机查杀。
特征码查杀是杀毒软件厂商查杀病毒的较为原始、准确率较高的一种方法。该方法是通过从病毒体内提取病毒特征码,从而能够有效识别出病毒。这种方法只能查杀已知病毒,对未知病毒则无能为力。
启发式查杀是静态地通过一系列“带权规则组合”对文件进行判定,如果值高于某个界限,则被认定为病毒,否则不为病毒。启发式查杀可以相对有效地识别出病毒,但是往往也会出现误报的情况。
虚拟机查杀技术主要是对付加密变形病毒而设计的。病毒被加密变形后存在多种形态,其变形的密钥不同,被称为多态型病毒。这样就无法提取特定的特征码进行查杀。但是加密之后的代码必须还原后才能进行运行,而解密代码是不变的。因此杀毒软件模拟出CPU的指令系统,模拟执行并还原加密后的病毒,当病毒还原后进行查杀。
启发式查杀和虚拟机查杀都是较为流行的查杀技术。
3. 简单病毒行为分析
这里编写一个简单的病毒专杀工具,这个工具非常简单。先准备一个简单的病毒样例,然后在虚拟机中进行一次行为分析,最后写出一个简单的病毒专杀工具。这次要对病毒进行行为分析,然后编写代码,完成专杀工具。
在虚拟机中进行病毒分析,因此安装虚拟机是一个必需的步骤。虚拟机也是一个软件,它用来模拟计算机的硬件,在虚拟机中可以安装操作系统,安装好操作系统后可以安装各种各样的应用软件,与操作真实的计算机是没有任何区别的。在虚拟机中的操作完全不影响真实的系统。除了对病毒进行分析需要安装虚拟机以外,在进行双机调试系统内核时安装虚拟机也是不错的选择。在虚拟机中安装其他种类的操作系统也非常方便。总之,使用虚拟机的好处非常多。这里推荐使用VMware虚拟机,请大家自行选择进行安装。
安装好虚拟机以后,在虚拟机上放置几个行为分析的工具,包括FileMon、RegMon和Procexp3个工具。分别对几个工具进行设置,对RegMon和FileMon进行字体设置,并设置过滤选项,如图1和图2所示。

图1 对RegMon设置过滤

图2 对FileMon设置过滤
对于FileMon和RegMon的字体设置,在菜单“选项”→“字体”命令下,通常选择“宋体”、“9号”。可以根据自己的喜好进行设置。在设置过滤条件时,在“包含”处输入的是需要监控的文件,这里的“a2.exe”是病毒的名字。也就是说,只监控与该病毒名相关的操作。
下面对Procexp进行设置,需要在进程创建或关闭时持续5秒高亮显示进程,以进行观察。设置方法为单击菜单“Options”→“Difference Highlight Duration”命令,在弹出的对话框中设置“Different Highlight Duration”为“5”,如图3所示。

图3 对Procexp的设置
将上面几个工具都设置完后,运行病毒,观察几个工具的反应,如图4、图5和图6所示。

图4 在Procexp中看到mirwzntk.exe病毒进程

图5 RegMon对注册表监控的信息

图6 FileMon对文件监控的信息
在图4中看到的病毒进程名为“mirwzntk.exe”,而不是“a2.exe”。这个进程是病毒“a2.exe”创建的,在病毒做完其相关工作后,将自己删除。图5和图6所示分别是病毒对注册表和文件进行的操作,这里就不一一进行说明了。下面对行为分析做个总结。
病毒在注册中写入了一个值,内容为“mirwznt.dll”,写入的位置如下:HKLM\SOFTWA RE\MICROSOFT\WINDOWSNT\CURRENTVERSION\WINDOWS\APPINIT_DLLS。病毒在C:\WINDOWS\system32\下创建了两个文件,分别是“mirwznt.dll”和“mirwzntk.exe”,创建病毒进程mirwzntk.exe,并生成了一个.bat的批处理程序用于删除自身,也就是删除“a2.exe”。
下面来写一个专杀工具,对该病毒进行查杀,如图7所示。

图7 mirwzntk病毒的专杀工具
4. 对mirwzntk病毒专杀工具的编写
在查杀病毒的技术中有一种方法类似特征码查杀法,这种方法并不从病毒体内提取特征码,而是计算病毒的散列值。也就是对病毒本身的内容进行散列计算,然后在查杀的过程中计算每个文件的散列,再进行比较。这种方法简单且易实现,常见的有MD5、CRC32等一些计算散列的算法。
下面选用CRC32算法计算函数的散列值,这里给出一个现成的CRC32函数,只需直接调用就可以了,代码如下:
DWORDCRC32(BYTE*ptr,DWORDSize){DWORDcrcTable[256],crcTmp1;//动态生成CRC32表for(inti=0;i<256;i++){crcTmp1=i;for(intj=8;j>0;j--){if(crcTmp1&1)crcTmp1=(crcTmp1>>1)^0xEDB88320L;elsecrcTmp1>>=1;}crcTable[i]=crcTmp1;}//计算CRC32值DWORDcrcTmp2=0xFFFFFFFF;while(Size--){crcTmp2=((crcTmp2>>8)&0x00FFFFFF)^crcTable[(crcTmp2^(*ptr))&0xFF];ptr++;}return(crcTmp2^0xFFFFFFFF);}
该函数的参数有两个,一个是指向缓冲区的指针,另一个是缓冲区的长度。将文件全部读入缓冲区内,然后用CRC32函数就可以计算文件的CRC32散列值。接着来看查杀的源代码,具体如下:
//查找指定进程BOOLFindTargetProcess(char*pszProcessName,DWORD*dwPid){BOOLbFind=FALSE;HANDLEhProcessSnap=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);if(hProcessSnap==INVALID_HANDLE_VALUE){returnbFind;}PROCESSENTRY32pe={0};pe.dwSize=sizeof(pe);BOOLbRet=Process32First(hProcessSnap,&pe);while(bRet){if(lstrcmp(pe.szExeFile,pszProcessName)==0){*dwPid=pe.th32ProcessID;bFind=TRUE;break;}bRet=Process32Next(hProcessSnap,&pe);}CloseHandle(hProcessSnap);returnbFind;}//提升权限BOOLEnableDebugPrivilege(char*pszPrivilege){HANDLEhToken=INVALID_HANDLE_VALUE;LUIDluid;TOKEN_PRIVILEGEStp;BOOLbRet=OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY,&hToken);if(bRet==FALSE){returnbRet;}bRet=LookupPrivilegeValue(NULL,pszPrivilege,&luid);if(bRet==FALSE){returnbRet;}tp.PrivilegeCount=1;tp.Privileges[0].Luid=luid;tp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED;bRet=AdjustTokenPrivileges(hToken,FALSE,&tp,sizeof(tp),NULL,NULL);returnbRet;}#defineEXE_VIRUS_NAME"mirwzntk.exe"#defineDLL_VIRUS_NAME"mirwznt.dll"voidCKillMirwzntkDlg::OnKill(){//在这里添加处理程序BOOLbRet=FALSE;DWORDdwPid=0;CStringcsTxt;bRet=FindTargetProcess("mirwzntk.exe",&dwPid);if(bRet==TRUE){csTxt=_T("检查系统内存...\r\n");csTxt+=_T("系统中存在病毒进程:mirwzntk.exe\r\n");csTxt+=_T("准备进行查杀...\r\n");SetDlgItemText(IDC_LIST,csTxt);bRet=EnableDebugPrivilege(SE_DEBUG_NAME);if(bRet==FALSE){csTxt+=_T("提升权限失败\r\n");}else{csTxt+=_T("提升权限成功\r\n");}SetDlgItemText(IDC_LIST,csTxt);HANDLEhProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPid);if(hProcess==INVALID_HANDLE_VALUE){csTxt+=_T("无法结束进程\r\n");return;}bRet=TerminateProcess(hProcess,0);if(bRet==FALSE){csTxt+=_T("无法结束进程\r\n");return;}csTxt+=_T("结束病毒进程\r\n");SetDlgItemText(IDC_LIST,csTxt);CloseHandle(hProcess);}else{csTxt+=_T("系统中不存在mirwzntk.exe病毒进程\r\n");}Sleep(10);charszSysPath[MAX_PATH]={0};GetSystemDirectory(szSysPath,MAX_PATH);lstrcat(szSysPath,"\\");lstrcat(szSysPath,EXE_VIRUS_NAME);csTxt+=_T("检查硬盘文件...\r\n");if(GetFileAttributes(szSysPath)==0xFFFFFFFF){csTxt+=_T("mirwzntk.exe病毒文件不存在\r\n");}else{csTxt+=_T("mirwzntk.exe病毒文件存在正在进行计算校验和\r\n");HANDLEhFile=CreateFile(szSysPath,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);if(hFile==INVALID_HANDLE_VALUE){MessageBox("CreateError");return;}DWORDdwSize=GetFileSize(hFile,NULL);if(dwSize==0xFFFFFFFF){MessageBox("GetFileSizeError");return;}BYTE*pFile=(BYTE*)malloc(dwSize);if(pFile==NULL){MessageBox("mallocError");return;}DWORDdwNum=0;ReadFile(hFile,pFile,dwSize,&dwNum,NULL);DWORDdwCrc32=CRC32(pFile,dwSize);if(pFile!=NULL){free(pFile);pFile=NULL;}CloseHandle(hFile);if(dwCrc32!=0x2A2F2D77){csTxt+=_T("校验和验证失败\r\n");}else{csTxt+=_T("校验和验证成功,正在删除...\r\n");bRet=DeleteFile(szSysPath);if(bRet){csTxt+=_T("mirwzntk.exe病毒被删除\r\n");}else{csTxt+=_T("mirwzntk.exe病毒无法被删除\r\n");}}}SetDlgItemText(IDC_LIST,csTxt);Sleep(10);GetSystemDirectory(szSysPath,MAX_PATH);lstrcat(szSysPath,"\\");lstrcat(szSysPath,DLL_VIRUS_NAME);if(GetFileAttributes(szSysPath)==0xFFFFFFFF){csTxt+=_T("mirwznt.dll病毒文件不存在\r\n");}else{csTxt+=_T("mirwznt.dll病毒文件存在正在进行计算校验和\r\n");HANDLEhFile=CreateFile(szSysPath,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);if(hFile==INVALID_HANDLE_VALUE){MessageBox("CreateError");return;}DWORDdwSize=GetFileSize(hFile,NULL);if(dwSize==0xFFFFFFFF){MessageBox("GetFileSizeError");return;}BYTE*pFile=(BYTE*)malloc(dwSize);if(pFile==NULL){MessageBox("mallocError");return;}DWORDdwNum=0;ReadFile(hFile,pFile,dwSize,&dwNum,NULL);DWORDdwCrc32=CRC32(pFile,dwSize);if(pFile!=NULL){free(pFile);pFile=NULL;}CloseHandle(hFile);if(dwCrc32!=0x2D0F20FF){csTxt+=_T("校验和验证失败\r\n");}else{csTxt+=_T("校验和验证成功,正在删除...\r\n");bRet=DeleteFile(szSysPath);if(bRet){csTxt+=_T("mirwznt.dll病毒被删除\r\n");}else{csTxt+=_T("mirwznt.dll病毒无法被删除\r\n");}}}Sleep(10);csTxt+=_T("正在检查注册表...\r\n");SetDlgItemText(IDC_LIST,csTxt);HKEYhKey;charcData[MAXBYTE]={0};LONGlSize=MAXBYTE;longlRet=RegOpenKey(HKEY_LOCAL_MACHINE,"SOFTWARE\\Microsoft\\WindowsNT\\CurrentVersion\\Windows",&hKey);if(lRet==ERROR_SUCCESS){lRet=RegQueryValueEx(hKey,"AppInit_DLLs",NULL,NULL,(unsignedchar*)cData,(unsignedlong*)&lSize);if(lRet==ERROR_SUCCESS){if(lstrcmp(cData,"mirwznt.dll")==0){csTxt+=_T("注册表项中存在病毒信息\r\n");}lRet=RegDeleteValue(hKey,"AppInit_DLLs");if(lRet==ERROR_SUCCESS){csTxt+=_T("注册表项中的病毒信息已删除\r\n");}else{csTxt+=_T("注册表项中的病毒信息无法删除\r\n");}}else{csTxt+=_T("注册表存项中不存在病毒信息\r\n");}RegCloseKey(hKey);}else{csTxt+=_T("注册表信息读取失败\r\n");}csTxt+=_T("病毒检测完成,请使用专业杀毒软件进行全面扫描\r\n");SetDlgItemText(IDC_LIST,csTxt);}
5. 感染型病毒的分析方法
查杀感染型的病毒是不能单单通过行为分析来完成的。查杀感染型的病毒最重要的是修复被感染后的文件,如果程序被感染了,而杀毒软件直接把文件删除了,这样做是不行的。对于专杀工具而言,就更不行了。下面介绍被感染后的可执行文件如何进行修复。
拿到一个感染型的病毒,要如何进行分析呢?大概可以分为如下几步。
首先是查看二进制文件的入口所在的节。通常情况下,文件的入口都会在PE程序的第一个节,比如在“.text”节或者是“CODE”节中,早期的启发式查杀中就会判定文件的入口在哪个节,如果不在第一节而在最后一节,就会被标注为可疑程序(当时这样做是因为加壳的程序较少,而现在这样做显然已经不合适了)。
其次是查看入口处的代码。通常情况下,程序启动都不会先执行程序员编写的代码,而是会先执行编译器产生的代码。在VC6的程序被执行后,首先执行的是启动函数。被首先执行的编译器的启动代码往往是固定的(每个相同版本的编译器的启动代码基本是相同、有规律可循的)。熟悉常见程序启动代码的情况下,可以通过识别入口处代码来判断文件是否感染病毒。感染型的代码一般都会在原始程序代码执行之前先执行。
再次是在虚拟机中调试病毒,找到原始程序的入口处,并将原始程序从内存中转存到磁盘上。在调试的过程中分析病毒代码执行的动作很重要,尤其是病毒进行加密之后,观察其解密还原非常重要。
最后是修复被感染的程序,包括可执行程序的入口、可执行程序的导入表,并删除病毒在文件中的代码。当程序从内存中转存到磁盘上之后,可能是无法运行的,那么就需要通过二进制比较,比较病毒与可执行程序存在的差异,逐步进行修复,直到转存后的程序可以被执行为止。最后一步是一个反复的工程,因为修复后的文件很可能无法使用,因此必须反复调试、比较文件才能完成。
以上就是感染型病毒的基本分析方法,在下面的实例中会介绍这些方法。
6. 感染型病毒的分析与专杀实现
这里有一个感染型病毒的两个样本,也就是一个病毒感染后的两个被感染的文件。它还会不断感染系统中其他文件,释放一些DLL之类的模块。但是,这里只关心如何去修复它被感染的部分。先来看专杀工具完成后的效果,如图8所示。

图8 感染型病毒专杀工具
首先用PEID查看可执行文件的入口点,发现可执行文件的入口点在“.text”节中,但是并没有识别出文件是被何种程序开发的,如图9所示。再通过观察节判断程序的开发工具,如图10所示。从图10中发现一个未知的节,即最后一节“.rrdata”。这个节名并非编译器生成的节名。用PEID再次查看另外一个样本的节信息,如图11所示。这里也有一个未知的节,同样是最后一个节,但是名称是“QDATA”。看来病毒每次感染文件后生成的节名并不相同,使用了随机节名的方法。

图9 用PEID查看入口所在节

图10 发现节表中有可疑节“.rrdata”

图11 另一个样本中的可疑节“.QDATA”
在没改变入口的情况下感染可执行文件的方法,可能是修改了程序的前几个字节,形成了类似Inline Hook的jmp的方法。另外的一个方法是将入口代码全部搬走。用OD打开其中一个样本,反汇编代码如下:
004031DF>$60pushad004031E0.E854000000call00403239004031E5.8DBD00104000leaedi,dwordptr[ebp+401000]004031EB.B8216E0000moveax,6E21004031F0.03F8addedi,eax004031F2.8BF7movesi,edi004031F4.50pusheax004031F5.9Bwait004031F6.DBE3finit004031F8.6833104000push00401033004031FD.55pushebp004031FE.DB0424filddwordptr[esp]00403201.DB442404filddwordptr[esp+4]00403205.DEC1faddpst(1),st
按F8键单步一下,然后在命令栏里输入hr esp并按回车键。按F9键运行程序,程序停止在如下代码处:
0040A243B8DF314000moveax,<模块入口点>0040A248FFE0jmpeax0040A24A43incebx0040A24B3A5C5749cmpbl,byteptr[edi+edx*2+49]0040A24F4Edecesi0040A25044incesp0040A2514Fdecedi
可以看到,0040A243地址处的代码显示为mov eax, <模块入口点>,这里的<模块入口点>是OD自动分析出来的。
在命令栏里输入hd esp并按回车键,按F8键分别执行0040A243和0040A248指令,来到如下代码处:
004031DF>$6A00push0004031E1?E830010000call<jmp.&kernel32.GetModuleHandleA>004031E6?A37C504000movdwordptr[40507C],eax004031EB.E814010000call<jmp.&kernel32.GetCommandLineA>004031F0.97xchgeax,edi004031F1?47incedi004031F2.B904010000movecx,104004031F7?B022moval,22004031F9?F2:AErepnescasbyteptres:[edi]004031FB?47incedi004031FC?803F22cmpbyteptr[edi],22004031FF?750Ajnzshort0040320B
可以看到,代码执行到了004031DF处。
004031DF的地址和用OD刚打开病毒时的地址是相同的,说明病毒又跳转回来执行了。观察这个代码,发现是ASM的启动代码。看来病毒是将原始程序的入口代码又写入程序的原入口点处让其执行了。
按Ctrl+F2组合键重新开始调试该病毒程序,在数据窗口来到入口代码处,并且下一个“内存写入”的断点。
为什么这么做呢?因为通过上面的分析,004031DF地址处的代码两次被执行,但是两次的代码不相同,就要知道是哪里修改了此处地址的代码。设置好内存写入断点后按F9键运行,被中断在0040A023地址处,反汇编代码如下:
0040A016B935000000movecx,350040A01B8DB54A124000leaesi,dwordptr[ebp+40124A]0040A0218BF8movedi,eax0040A023F3:66:A5repmovswordptres:[edi],wordptr[esi];中断在这里0040A02633DBxorebx,ebx0040A02864:67:8B1E3000movebx,dwordptrfs:[30]0040A02E85DBtestebx,ebx
观察EDI寄存器的值,刚好是004031DF,也就是程序的入口地址。
选中0040A026地址,按F2键设置一个断点,将刚才设置的“内存写入”断点删除,按F8键单击一下,让程序停止在0040A026地址处。
为什么要在0040A026上设置一个断点再按F8键呢?因为调试的时候发现,如果直接按F8键的话,程序就跑到别处了。
当程序停止在0040A026地址处时,将该处的断点取消。在反汇编窗口中查看004031DF,发现已经将ASM程序的入口代码还原了。
按Ctrl+F2键重新开始调试该病毒程序,直接查看0040A023地址处的代码,反汇编代码如下:
0040A020005628addbyteptr[esi+28],dl0040A02311C6adcesi,eax0040A0254Ddecebp0040A02643incebx0040A0273524939BE4xoreax,E49B93240040A02CD0FFsarbh,1
和刚才的代码不相同,看来这段代码是被加密处理的。这段代码的解密是由病毒的入口代码完成的,当入口代码将加密后的代码还原后,直接由00403237的跳转代码跳转而来,代码如下:
0040322B.D1E9shrecx,10040322D.66:ABstoswordptres:[edi]0040322F.E212loopdshort0040324300403231.81EFFC4F0000subedi,4FFC00403237.FFE7jmpedi00403239$8B2C24movebp,dwordptr[esp]0040323C81db81
00403237是病毒入口解密代码的最后一句代码,当该句jmp被执行后,来到如下代码处:
0040A00458popeax0040A00558popeax0040A006B800104000moveax,004010000040A00B03C5addeax,ebp0040A00D5Bpopebx0040A00E03EBaddebp,ebx0040A010898544124000movdwordptr[ebp+401244],eax0040A016B935000000movecx,350040A01B8DB54A124000leaesi,dwordptr[ebp+40124A]0040A0218BF8movedi,eax0040A023F3:66:A5repmovswordptres:[edi],wordptr[esi]0040A02633DBxorebx,ebx0040A02864:67:8B1E3000movebx,dwordptrfs:[30]0040A02E85DBtestebx,ebx0040A030780Ejsshort0040A040
可以看到,0040A023就是还原ASM程序入口代码的rep movs指令。
仍然在0040A026地址处按F2键设置断点,按F9键执行到0040A026地址处,然后取消F2断点。观察EAX发现,此时EAX即为入口地址,那么修改0040A026地址处的代码为jmp eax。按F8键单步执行jmp eax指令,即来到已经还原好的入口地址。到了入口处,即可将程序从内存中转存到磁盘上。
在OD的菜单栏中选择“插件->OllyDump-> Dump debugged process”,出现图12所示的窗口。单击“Dump”按钮,选择保存的位置,保存名为“dump.exe”,这样就将内存中的程序转存到了磁盘文件上。执行dump.exe程序,发现可以执行。用OD打开该程序,发现入口处是类似ASM的入口代码。

图12 转存窗口
下面编程来完成病毒的扫描和转存。为了能够识别被此类病毒感染的程序,需要在样本文件中提取特征码。提取的特征码是样本文件的入口代码,病毒样本入口的代码是用来解密还原真正病毒的代码,而每个样本的解密密钥是不同的,其差异只有4字节。因此分两段来进行匹配,提取的特征码如下:
#defineVIRUS_SIGN_LEN0x66charszVirusSign1[]="\x60\xE8\x54\x00\x00\x00\x8D\xBD\x00\x10\x40\x00\xB8";charszVirusSign2[]="\x00\x03\xF8\x8B\xF7\x50\x9B\xDB\xE3\x68\x33\x10\x40\x00\x55\xDB""\x04\x24\xDB\x44\x24\x04\xDE\xC1\xDB\x1C\x24\x8B\x14\x24\xB9\x00""\x28\x00\x00\x66\xAD\x89\x0C\x24\xDB\x04\x24\xDA\x8D\x66\x10\x40""\x00\xDB\x1C\x24\xD1\xE1\x29\x0C\x24\x33\x04\x24\xD1\xE9\x66\xAB""\xE2\x12\x81\xEF\xFC\x4F\x00\x00\xFF\xE7\x8B\x2C\x24\x81\xED\x06""\x10\x40\x00\xC3\xFF\xE2";
扫描文件匹配特征码时,需要先判断文件是否为有效的 PE 文件,然后才进行特征码匹配。特征码匹配的代码如下:
BOOLCKillOleMdb32Dlg::IsVirFile(CStringstrPath){BOOLbRet=FALSE;HANDLEhFile=CreateFile(strPath.GetBuffer(0),GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);HANDLEhMap=CreateFileMapping(hFile,NULL,PAGE_READONLY,0,0,NULL);LPVOIDlpBase=MapViewOfFile(hMap,FILE_MAP_READ,0,0,0);PIMAGE_NT_HEADERSpImgNtHdr=ImageNtHeader(lpBase);DWORDdwEntryPoint=pImgNtHdr->OptionalHeader.AddressOfEntryPoint;PIMAGE_SECTION_HEADERpImgSecHdr=ImageRvaToSection(pImgNtHdr,lpBase,dwEntryPoint);//定位入口地址在文件中的地址DWORDdwFA=((dwEntryPoint-pImgSecHdr->VirtualAddress+pImgSecHdr->PointerToRawData)+(DWORD)lpBase);//比较第一段特征码if(memcmp((constvoid*)dwFA,(constvoid*)szVirusSign1,13)==0){dwFA+=0x10;//比较第二段特征码if(memcmp((constvoid*)dwFA,(constvoid*)szVirusSign2,0x66-0x10)==0){bRet=TRUE;}}UnmapViewOfFile(lpBase);CloseHandle(hMap);CloseHandle(hFile);returnbRet;}
匹配特征码的函数返回值是BOOL类型。如果发现当前扫描的程序的入口代码与病毒的特征码相同,那么就会返回TRUE,从而进行后续的处理工作。
对病毒的修复都封装在一个CRepairPe类中,通过构造函数将病毒文件的完整路径传递给该类,由构造函数调用一些列的相关函数进行处理。现在要完成的功能是将已经还原好的入口代码从内存中转存到磁盘文件上,主要看CRepairePe:: DumpVir(char *strVir);函数:
BOOLCRepairPe::DumpVir(char*strVir){STARTUPINFOsi;PROCESS_INFORMATIONpi;si.cb=sizeof(si);GetStartupInfo(&si);DEBUG_EVENTde={0};CONTEXTcontext={0};//创建病毒进程BOOLbRet=CreateProcess(strVir,NULL,NULL,NULL,FALSE,DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&si,&pi);CloseHandle(pi.hThread);CloseHandle(pi.hProcess);BYTEbCode;DWORDdwNum;//第几次断点intnCc=0;while(TRUE){//开始调试循环WaitForDebugEvent(&de,INFINITE);switch(de.dwDebugEventCode){caseCREATE_PROCESS_DEBUG_EVENT:{//计算入口地址+0x58的地址//即解密还原完后续病毒处的//jmpedi的地址处DWORDdwAddr=0x58+(DWORD)de.u.CreateProcessInfo.lpStartAddress;//暂停线程SuspendThread(de.u.CreateProcessInfo.hThread);//读取入口地址+0x58地址处的字节码ReadProcessMemory(de.u.CreateProcessInfo.hProcess,(constvoid*)dwAddr,&bCode,sizeof(BYTE),&dwNum);//在入口地址+0x58地址处写入0xCC//即写入INT3WriteProcessMemory(de.u.CreateProcessInfo.hProcess,(void*)dwAddr,&bCC,sizeof(BYTE),&dwNum);//恢复线程ResumeThread(de.u.CreateProcessInfo.hThread);break;}caseEXCEPTION_DEBUG_EVENT:{switch(nCc){case0:{//第0次的断点是系统断点//这里忽略nCc++;break;}case1:{OneCc(&de,&bCode);nCc++;break;}case2:{TwoCc(&de,&bCode);nCc++;break;}case3:{ThreeCc(&de,&bCode);nCc++;gotoend0;break;}case4:{nCc++;gotoend0;}}}}ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_CONTINUE);}end0:bRet=TRUE;returnbRet;}
在触发创建进程的异常时,在jmp edi处设置断点,因为到了jmp edi时,真正的病毒代码已经解密完成了。根据调试病毒的情况,需要处理3次断点。第一次处理断点的代码如下:
VOIDCRepairPe::OneCc(DEBUG_EVENT*pDe,BYTE*bCode){//在jmpedi处断下后//首先需要恢复jmpedi原来的字节码//然后在还原完原入口代码的下一句代码处//即xorebx,ebx处设置断点CONTEXTcontext;DWORDdwNum;HANDLEhProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,pDe->dwProcessId);HANDLEhThread=OpenThread(THREAD_ALL_ACCESS,FALSE,pDe->dwThreadId);SuspendThread(hThread);BYTEbTmp;ReadProcessMemory(hProcess,pDe->u.Exception.ExceptionRecord.ExceptionAddress,&bTmp,sizeof(BYTE),&dwNum);context.ContextFlags=CONTEXT_FULL;GetThreadContext(hThread,&context);context.Eip--;WriteProcessMemory(hProcess,(void*)context.Eip,bCode,sizeof(BYTE),&dwNum);SetThreadContext(hThread,&context);DWORDdwEdi=context.Edi+0x22;ReadProcessMemory(hProcess,(constvoid*)dwEdi,bCode,sizeof(BYTE),&dwNum);WriteProcessMemory(hProcess,(void*)dwEdi,&bCC,sizeof(BYTE),&dwNum);SetThreadContext(hThread,&context);ResumeThread(hThread);CloseHandle(hThread);CloseHandle(hProcess);}
第二次处理断点的代码如下:
VOIDCRepairPe::TwoCc(DEBUG_EVENT*pDe,BYTE*bCode){//在xorebx,ebx处断下后//首先需要恢复xorebx,ebx原来的字节码//然后修改xorebx,ebx为jmpeax//并在入口点设置断点CONTEXTcontext;DWORDdwNum;ANDLEhProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,pDe->dwProcessId);HANDLEhThread=OpenThread(THREAD_ALL_ACCESS,FALSE,pDe->dwThreadId);SuspendThread(hThread);BYTEbTmp;ReadProcessMemory(hProcess,pDe->u.Exception.ExceptionRecord.ExceptionAddress,&bTmp,sizeof(BYTE),&dwNum);context.ContextFlags=CONTEXT_FULL;GetThreadContext(hThread,&context);context.Eip--;WriteProcessMemory(hProcess,(void*)context.Eip,bJmp,sizeof(BYTE)*2,&dwNum);ReadProcessMemory(hProcess,(constvoid*)context.Eax,bCode,sizeof(BYTE),&dwNum);WriteProcessMemory(hProcess,(void*)context.Eax,&bCC,sizeof(BYTE),&dwNum);SetThreadContext(hThread,&context);ResumeThread(hThread);CloseHandle(hThread);CloseHandle(hProcess);}
第三次处理断点的代码如下:
VOIDCRepairPe::ThreeCc(DEBUG_EVENT*pDe,BYTE*bCode){//在入口点断下后//恢复入口点的代码//然后开始dumpCONTEXTcontext;DWORDdwNum;HANDLEhProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,pDe->dwProcessId);HANDLEhThread=OpenThread(THREAD_ALL_ACCESS,FALSE,pDe->dwThreadId);SuspendThread(hThread);BYTEbTmp;ReadProcessMemory(hProcess,pDe->u.Exception.ExceptionRecord.ExceptionAddress,&bTmp,sizeof(BYTE),&dwNum);context.ContextFlags=CONTEXT_FULL;GetThreadContext(hThread,&context);WriteProcessMemory(hProcess,pDe->u.Exception.ExceptionRecord.ExceptionAddress,bCode,sizeof(BYTE),&dwNum);context.Eip--;SetThreadContext(hThread,&context);Dump(pDe,context.Eip);ResumeThread(hThread);CloseHandle(hThread);CloseHandle(hProcess);}
第三次处理入口的断点时,需要把整个文件从内存转存到磁盘文件上,需调用CRepairPe:: Dump(DEBUG_EVENT *pDe, DWORD dwEntryPoint);函数,其代码如下:
VOIDCRepairPe::Dump(DEBUG_EVENT*pDe,DWORDdwEntryPoint){DWORDdwPid=pDe->dwProcessId;MODULEENTRY32me32;HANDLEhSnap=CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,dwPid);me32.dwSize=sizeof(MODULEENTRY32);BOOLbRet=Module32First(hSnap,&me32);HANDLEhFile=CreateFile(me32.szExePath,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);//判断PE文件的有效性IMAGE_DOS_HEADERimgDos={0};DWORDdwReadNum=0;ReadFile(hFile,&imgDos,sizeof(IMAGE_DOS_HEADER),&dwReadNum,NULL);SetFilePointer(hFile,imgDos.e_lfanew,0,FILE_BEGIN);IMAGE_NT_HEADERSimgNt={0};ReadFile(hFile,&imgNt,sizeof(IMAGE_NT_HEADERS),&dwReadNum,NULL);//得到EXE文件的大小DWORDBaseSize=me32.modBaseSize;if(imgNt.OptionalHeader.SizeOfImage>BaseSize){BaseSize=imgNt.OptionalHeader.SizeOfImage;}LPVOIDpBase=VirtualAlloc(NULL,BaseSize,MEM_COMMIT,PAGE_READWRITE);HANDLEhProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPid);//读取文件的数据bRet=ReadProcessMemory(hProcess,me32.modBaseAddr,pBase,me32.modBaseSize,NULL);PIMAGE_DOS_HEADERpDos=(PIMAGE_DOS_HEADER)pBase;PIMAGE_NT_HEADERSpNt=(PIMAGE_NT_HEADERS)(pDos->e_lfanew+(PBYTE)pBase);//设置文件的入口地址pNt->OptionalHeader.AddressOfEntryPoint=dwEntryPoint-pNt->OptionalHeader.ImageBase;//设置文件的对齐方式pNt->OptionalHeader.FileAlignment=0x1000;PIMAGE_SECTION_HEADERpSec=(PIMAGE_SECTION_HEADER)((PBYTE)&pNt->OptionalHeader+pNt->FileHeader.SizeOfOptionalHeader);for(inti=0;i<pNt->FileHeader.NumberOfSections;i++){pSec->PointerToRawData=pSec->VirtualAddress;pSec->SizeOfRawData=pSec->Misc.VirtualSize;pSec++;}CloseHandle(hFile);m_StrVirm_StrVir=m_StrVir.Left(m_StrVir.ReverseFind('\\'));m_StrVir+="\\dump.exe";hFile=CreateFile(m_StrVir.GetBuffer(0),GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);DWORDdwWriteNum=0;//将读取的数据写入文件bRet=WriteFile(hFile,pBase,me32.modBaseSize,&dwWriteNum,NULL);CloseHandle(hFile);VirtualFree(pBase,me32.modBaseSize,MEM_RELEASE);CloseHandle(hProcess);CloseHandle(hSnap);}
执行转存出来的dump.exe程序,发现是可以运行的。用PEID进行PE识别,可以识别出是ASM写的程序,如图13所示。

图13 正确识别可执行文件
接着要修复导入表信息,用PEID对比导入表的信息,如图14所示。

图14 转存前后导入表信息比较
要处理该部分非常简单,只要把转存前的导入信息赋值给转存后的导入信息即可,具体代码如下:
VOIDCRepairPe::BuildIat(char*pSrc,char*pDest){getchar();PIMAGE_DOS_HEADERpSrcImgDosHdr,pDestImgDosHdr;PIMAGE_NT_HEADERSpSrcImgNtHdr,pDestImgNtHdr;PIMAGE_SECTION_HEADERpSrcImgSecHdr,pDestImgSecHdr;PIMAGE_IMPORT_DESCRIPTORpSrcImpDesc,pDestImpDesc;HANDLEhSrcFile,hDestFile;HANDLEhSrcMap,hDestMap;LPVOIDlpSrcBase,lpDestBase;hSrcFile=CreateFile(pSrc,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);hDestFile=CreateFile(pDest,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);hSrcMap=CreateFileMapping(hSrcFile,NULL,PAGE_READONLY,0,0,0);hDestMap=CreateFileMapping(hDestFile,NULL,PAGE_READWRITE,0,0,0);lpSrcBase=MapViewOfFile(hSrcMap,FILE_MAP_READ,0,0,0);lpDestBase=MapViewOfFile(hDestMap,FILE_MAP_WRITE,0,0,0);pSrcImgDosHdr=(PIMAGE_DOS_HEADER)lpSrcBase;pDestImgDosHdr=(PIMAGE_DOS_HEADER)lpDestBase;pSrcImgNtHdr=(PIMAGE_NT_HEADERS)((DWORD)lpSrcBase+pSrcImgDosHdr->e_lfanew);pDestImgNtHdr=(PIMAGE_NT_HEADERS)((DWORD)lpDestBase+pDestImgDosHdr->e_lfanew);pSrcImgSecHdr=(PIMAGE_SECTION_HEADER)((DWORD)&pSrcImgNtHdr->OptionalHeader+pSrcImgNtHdr->FileHeader.SizeOfOptionalHeader);pDestImgSecHdr=(PIMAGE_SECTION_HEADER)((DWORD)&pDestImgNtHdr->OptionalHeader+pDestImgNtHdr->FileHeader.SizeOfOptionalHeader);DWORDdwImpSrcAddr,dwImpDestAddr;dwImpSrcAddr=pSrcImgNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;dwImpDestAddr=pDestImgNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;dwImpSrcAddr=(DWORD)lpSrcBase+Rva2Fa(pSrcImgNtHdr,lpSrcBase,dwImpSrcAddr);dwImpDestAddr=(DWORD)lpDestBase+Rva2Fa(pDestImgNtHdr,lpDestBase,dwImpDestAddr);//定位导入表pSrcImpDesc=(PIMAGE_IMPORT_DESCRIPTOR)dwImpSrcAddr;pDestImpDesc=(PIMAGE_IMPORT_DESCRIPTOR)dwImpDestAddr;PIMAGE_THUNK_DATApSrcImgThkDt,pDestImgThkDt;intn=0;while(pSrcImpDesc->Name&&pDestImpDesc->Name){n++;char*pSrcImpName=(char*)((DWORD)lpSrcBase+Rva2Fa(pSrcImgNtHdr,lpSrcBase,pSrcImpDesc->Name));char*pDestImpName=(char*)((DWORD)lpDestBase+Rva2Fa(pDestImgNtHdr,lpDestBase,pDestImpDesc->Name));pSrcImgThkDt=(PIMAGE_THUNK_DATA)((DWORD)lpSrcBase+Rva2Fa(pSrcImgNtHdr,lpSrcBase,pSrcImpDesc->FirstThunk));pDestImgThkDt=(PIMAGE_THUNK_DATA)((DWORD)lpDestBase+Rva2Fa(pDestImgNtHdr,lpDestBase,pDestImpDesc->FirstThunk));//赋值信息while(*((DWORD*)pSrcImgThkDt)&&*((DWORD*)pDestImgThkDt)){DWORDdwIatAddr=*((DWORD*)pSrcImgThkDt);*((DWORD*)pDestImgThkDt)=dwIatAddr;pSrcImgThkDt++;pDestImgThkDt++;}pSrcImpDesc++;pDestImpDesc++;}UnmapViewOfFile(lpDestBase);UnmapViewOfFile(lpSrcBase);CloseHandle(hDestMap);CloseHandle(hSrcMap);CloseHandle(hDestFile);CloseHandle(hSrcFile);}
修复后再次使用PEID进行对比,发现已经相同了。最后一步就是要将病毒所在的节数据移除,移除后对应的节表信息、节数量信息、映像大小信息等都要进行调整,具体代码如下:
VOIDCRepairPe::Repair(char*pSrc,char*pDest){PIMAGE_DOS_HEADERpSrcImgDosHdr;PIMAGE_NT_HEADERSpSrcImgNtHdr;PIMAGE_SECTION_HEADERpSrcImgSecHdr;PIMAGE_SECTION_HEADERpSrcLastImgSecHdr;WORDwSrcSecNum,wDestSecNum;HANDLEhSrcFile=CreateFile(pSrc,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);HANDLEhSrcMap=CreateFileMapping(hSrcFile,NULL,PAGE_READONLY,0,0,NULL);LPVOIDlpSrcBase=MapViewOfFile(hSrcMap,FILE_MAP_READ,0,0,0);pSrcImgDosHdr=(PIMAGE_DOS_HEADER)lpSrcBase;pSrcImgNtHdr=(PIMAGE_NT_HEADERS)((DWORD)lpSrcBase+pSrcImgDosHdr->e_lfanew);pSrcImgSecHdr=(PIMAGE_SECTION_HEADER)((DWORD)&pSrcImgNtHdr->OptionalHeader+pSrcImgNtHdr->FileHeader.SizeOfOptionalHeader);wSrcSecNum=pSrcImgNtHdr->FileHeader.NumberOfSections;pSrcLastImgSecHdr=pSrcImgSecHdr+wSrcSecNum-1;DWORDdwSrcFileSize=GetFileSize(hSrcFile,NULL);DWORDdwSrcSecSize=pSrcLastImgSecHdr->PointerToRawData+pSrcLastImgSecHdr->SizeOfRawData;LPVOIDlpBase;DWORDdwNum;if(dwSrcFileSize>dwSrcSecSize){lpBase=VirtualAlloc(NULL,(dwSrcFileSize-dwSrcSecSize),MEM_COMMIT,PAGE_READWRITE);SetFilePointer(hSrcFile,dwSrcSecSize,NULL,FILE_BEGIN);ReadFile(hSrcFile,lpBase,(dwSrcFileSize-dwSrcSecSize),&dwNum,NULL);}UnmapViewOfFile(lpSrcBase);CloseHandle(hSrcMap);CloseHandle(hSrcFile);HANDLEhDestFile=CreateFile(pDest,GENERIC_ALL,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);IMAGE_DOS_HEADERImgDosHdr;IMAGE_NT_HEADERSImgNtHdr;IMAGE_SECTION_HEADERImgSecHdr,ImgSecHdr1,ImgSecHdr2;ReadFile(hDestFile,&ImgDosHdr,sizeof(IMAGE_DOS_HEADER),&dwNum,NULL);SetFilePointer(hDestFile,ImgDosHdr.e_lfanew,NULL,FILE_BEGIN);ReadFile(hDestFile,&ImgNtHdr,sizeof(IMAGE_NT_HEADERS),&dwNum,NULL);wDestSecNum=ImgNtHdr.FileHeader.NumberOfSections;SetFilePointer(hDestFile,sizeof(IMAGE_SECTION_HEADER)*(wDestSecNum-2),NULL,FILE_CURRENT);ReadFile(hDestFile,&ImgSecHdr2,sizeof(IMAGE_SECTION_HEADER),&dwNum,NULL);ReadFile(hDestFile,&ImgSecHdr,sizeof(IMAGE_SECTION_HEADER),&dwNum,NULL);SetFilePointer(hDestFile,((-1)*sizeof(IMAGE_SECTION_HEADER)),NULL,FILE_CURRENT);ZeroMemory(&ImgSecHdr1,sizeof(IMAGE_SECTION_HEADER));WriteFile(hDestFile,&ImgSecHdr1,sizeof(IMAGE_SECTION_HEADER),&dwNum,NULL);DWORDdwFileEnd=ImgSecHdr.PointerToRawData;SetFilePointer(hDestFile,dwFileEnd,NULL,FILE_BEGIN);SetEndOfFile(hDestFile);SetFilePointer(hDestFile,0,NULL,FILE_END);WriteFile(hDestFile,lpBase,(dwSrcFileSize-dwSrcSecSize),&dwNum,NULL);ImgNtHdr.FileHeader.NumberOfSections--;ImgNtHdr.OptionalHeader.SizeOfImage=ImgSecHdr2.VirtualAddress+Align(ImgSec-Hdr2.Misc.VirtualSize,ImgNtHdr.OptionalHeader.SectionAlignment);SetFilePointer(hDestFile,ImgDosHdr.e_lfanew,NULL,FILE_BEGIN);WriteFile(hDestFile,&ImgNtHdr,sizeof(IMAGE_NT_HEADERS),&dwNum,NULL);CloseHandle(hDestFile);}
修复完成后再次运行修复的病毒程序,发现是可以运行的。然后比较它们的节数量及文件大小,可以看出都已经完善了。整个的病毒特征码扫描、病毒修复的代码就完成了。至于界面部分,这里就不给出代码了。
这里对感染型病毒的修复其实并不完美,虽然把它成功地修复了。为什么不完美呢?对于病毒代码的解密是动态完成的,它还原入口处的代码是在执行真正的病毒代码之前还原的。如果是在病毒代码之后还原的话,这样动态的方式去还原岂不是帮助所有感染的病毒程序运行了一次病毒代码?正因为是在之前运行的,因此采用了这种方法,而正确的方法是通过专杀程序去完成感染程序入口的还原。关于病毒专杀的内容就介绍到这里。