百度一下 藏锋者 就能快速找到本站! 每日资讯归档 下载藏锋者到桌面一键访问

当前位置:主页 > 网络安全 > VC编写简易杀毒软件

VC编写简易杀毒软件

所在栏目:网络安全 时间:04-13 00:25 分享:

  杀毒软件的编写,也许一直以来都是高手手中的法宝。我不是高手,也没有能力写一款杀毒软件出来。我的文章也没有新颖之处,说是“简易”,其实类似于大家编写的专杀工具,或者是一些国内个人防毒软件使用的技术。我只是简单地整理了一些大家在编写专杀时常常能使用到的代码,或许对于大家在写专杀或者写个人防毒软件时能用得着,毕竟善于总结和归纳也是一种学习的方法和提高的手段。
  我不知道编写一个防毒工具或专杀工具应该从哪个地方开始入手,也不知道要从哪部分开始介绍。算了,还是不去想先后顺序了,一部分一部分地介绍吧,只要能把我想说的都说完就可以了。
  首先从文件扫描开始吧。文件扫描是每个杀毒软件都有的,不管是“扫描全盘”也好,还是针对某个分区或是文件夹扫描也好,都要进行文件扫描才能完成查找病毒的任务。这个问题并不怎么难理解,只要是使用过杀毒软件或者使用过搜索文件工具的人都应该非常清楚。好吧,既然大家都了解了,那么就看看具体实现扫描的代码的编写吧。
  
DWORD WINAPI FindFiles(LPVOID lpszPath)
{……//此处省略了很多变量的定义
szFilter="*.*";  //定义扫描文件的类型,这里是所有文件
lstrcpy(szPath,(char *)lpszPath);
len=lstrlen(szPath);
if(szPath[len-1]!='\\')
{
szPath[len]='\\';
szPath[len+1]='\0';
}
lstrcpy(szSearch,szPath);
lstrcat(szSearch,szFilter);
hFindFile=FindFirstFile(szSearch,&stFindFile);
if(hFindFile!=INVALID_HANDLE_VALUE)
{
do
{
lstrcpy(szFindFile,szPath);
lstrcat(szFindFile,stFindFile.cFileName);
if(stFindFile.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
if(stFindFile.cFileName[0]!='.')
{
FindFiles(szFindFile);//递归扫描下层目录
}
}
else
{
printf("%s\n",szFindFile);
}
ret=FindNextFile(hFindFile,&stFindFile);
}while(ret!=0);
}
FindClose(hFindFile);
return 0;
}
  
  一切定义变量的部分我都省略了,大家可以看我随文提供的源代码。我把这个扫描文件的代码写成了一个函数,参数只有一个,就是要求输入一个要扫描的路径。代码里面也没有什么特殊的部分,只是个简单的循环,循环里面还有一个递归。递归是一个比较复杂的算法,“递归跳跃的信任”也是不容易掌握的。我对递归的应用可以说是差到了极点,好在这里不需要我来说明递归的使用方法,大家可以翻翻书去掌握这个算法。继续说我们上面的代码,代码里没有复杂的地方,大家只要查看MSDN中关于FindFirstFile()和FileNextFile()的使用方法就可以了。说白了,这两个函数就是扫描文件的两个关键函数了。
  虽然扫描文件的方法比较简单,但是有一点要提醒大家,算是编程中应该注意的问题。如果想做一个图形界面的文件扫描的程序的话,大家往往会为了人性化而在状态栏上显示一下当前正扫描到的文件名,或者在状态栏里做一个统计当前扫描完的文件个数的一个计数器,不管是文件的显示,还是计数的显示,都是一个实时过程,就是要随时更新显示。如果大家把扫描的函数写在主线程中(也就是跟窗口写在一个线程中),那么状态栏上的内容是无法被更新和显示的。因为扫描文件的循环会占去资源,而无法有机会去更新状态栏的显示。因此,大家在编写的时候一定要把扫描文件的函数放在另外一个工作线程中,让界面的主线程去启动扫描文件的工作线程,主线程就有机会去更新状态栏的显示了。
  除了要扫描文件以外,我们还要得到系统的硬盘和可移动磁盘,实现代码也比较简单。
  
char DriveName[4];
char *p;
p=DriveName;
strncpy(DriveName,"C:",4);
while(*p<'Z')
{
if(GetDriveType(p)==DRIVE_FIXED || GetDriveType(p)==DRIVE_REMOVABLE)
{
……//得到的硬盘和可移动磁盘的盘符
}
++*p;
}
  
  上面的代码果然是太简单了,简单到已经不用解释了。查阅一下MSDN中对GetDriverType()这个函数的使用方法就可以了。
  下面该介绍第二个问题了,也就是关于分析PE的问题。不管是分析病毒本身也好,还是分析被病毒感染的文件也好,PE文件头的分析都是十分重要的。我们上面扫描文件的时候会显示所有的文件,但是往往大多数病毒并不是所有的文件都感染,大多只是感染一些可执行文件(或者是感染指定的一些个文件)。因此,我们就要找到哪些文件是PE文件,找到后要对其进行分析后才进行感染。对于PE的分析是一个很复杂的事情,而我又不是一个高手,所以我只能简单地说说这部分里我所知道的。找到PE文件的方法是在PE头中找到“PE”字样,而这个方法在黑防中以前就有过介绍,可以顺序读取以找到“PE”标志,也可以通过PE的结构来定位“PE”标志。总之,方法不怎么难,而且在我以前的文章中也进行过介绍,这里就不多说了。
  在确定了是PE文件后,我认为需要分析PE文件的导入表。PE文件在使用API的时候,都是通过查找导入表来完成的。我们可以观察它的导入表,看是否能发现一些类似于SetWindowsHookExA()、SetWindowsHookExW()之类的函数。关于扫描导入表的方法,我在以前的文章中也介绍过了,大家可以参考一下。在扫描导入表的代码里加入以下代码,就可以知道该PE文件中是否有我们关注的API函数了。
  
//看是否有挂钩的函数
if(!lstrcmp((const char *)pImportByName->Name,"SetWindowsHookW")||!lstrcmp((const char *)pImportByName->Name,"SetWindowsHookExW")||!lstrcmp((const char *)pImportByName->Name,"SetWindowsHookExA")||!lstrcmp((const char *)pImportByName->Name,"SetWindowsHookExA"))
//看是否有创建远程线程的函数
if(!lstrcmp((const char *)pImportByName->Name,"CreateRemoteThread"))
  
  若有这些可疑的函数,那么这个PE文件就有可能是病毒或者是可疑的程序了。其实有一些国内的个人写的防毒软件里面真的有使用该方法来判断病毒的。不过这种方法无法发现用LoadLibrary()和GetProcAddress()来调用API的行为。有一点值得注意,如果在一个PE文件中,只有LoadLibrary()、GetProcAddress()和很少的几个API函数的话(也可能是导入的DLL很多,但是只使用了每个DLL的一、两个导出函数),不是中了病毒,就是被加了壳,大多数是加壳的可能性比较大一些。因为病毒很少会使用导入表中的函数,而是直接定位kernel32.dll的地址后来调用LoadLibrary()和GetProcAddress()使用API函数。
  判断PE文件程序的入口点(AddressOfEntryPoint)是否异常也是非常重要的,如果程序的入口点不是指向代码节就非常可疑了。很多病毒都会新添加一个节来存放代码(有些则是在PE的缝隙中存放代码),那么PE文件在执行时就是从最后一节开始执行的,因此也比较可疑,很可能是被病毒感染的PE文件了。
  除了分析导入表以外,还有很多关键的地方需要分析。比如节头部的属性是否可疑(代码节有“可写”的属性等);再比如有可疑的代码重定向,多个PE头等等。PE文件的分析远不止这么点,它是一个复杂的过程,而我也就只能说这么多了,再深入我也说不清楚或者根本就不懂了。
  在扫描并确定是病毒的时候,我们就要对病毒进行删除了。对文件进行删除时可以使用DeleteFile()函数,函数使用的方法很简单,只要把要删除的文件名传递给函数就可以完成操作了。其实在删除之前,我们还是少了一个步骤的,如果病毒在运行的话,我们就无法删除病毒了。因此必须到进程列表里找到病毒进程,或者病毒所依附的进程,把病毒进程杀掉,或者把病毒从进程里卸载出来。对进程的遍历是个非常简单的事情,代码也非常少。但是要杀掉一些进程时,必须有足够的权限。对!我们需要调整我们的程序的权限。实现代码如下。
  
HANDLEhToken;
LUIDuID;
TOKEN_PRIVILEGES tp;
OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,&hToken);
LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&uID);
tp.PrivilegeCount=1;
tp.Privileges[0].Luid=uID;
tp.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken,FALSE,&tp,sizeof(tp),NULL,NULL);
CloseHandle(hToken);
  
  这样就可以对我们要操作的进程执行操作了。当然,有一些系统进程我们用这种方法没有办法解决的。还有一种病毒,它会创建两个进程,一个进程是病毒本身,另一个进程是病毒的守护进程,两个进程相互保护从而达到不容易被轻易结束的效果。这种进程我们通过任务管理器无法做到关闭的。不过,我们也有办法解决,实现的关键在SuspendThread()函数上,这个函数可以暂停线程的运行。
  
b=Thread32First(hThreadSnap,&th32);
while(b)
{
if(th32.th32OwnerProcessID==Pid)
{
HANDLE oth=OpenThread(THREAD_ALL_ACCESS,FALSE,th32.th32ThreadID);
if(!(SuspendThread(oth)))
{
MessageBox("Onlock OK!");
}
CloseHandle(oth);
break;
}
Thread32Next(hThreadSnap,&th32);
}
CloseHandle(hThreadSnap);
  
  实现方法就是暂停守护进程和病毒进程的所有线程,这样就可以让它们无法工作,从而无法相互保护了。国内的某个个人防毒软件当中有个任务管理器,在任务管理器中实现了这个暂停进程的所有线程,从而冻结进程的功能。然后用TerminateProcess()函数结束掉这些进程,就可以再回到上一步中来执行我们的DeleteFile()函数了。如果病毒不是一个进程,而只是某个进程中的DLL的话,那么我们可以用CreateRemoteThread()函数和FreeLibrary()函数卸载掉病毒,然后再运行DeleteFile()函数就可以了。
  最后,我们可以使用一些操作注册表的相关函数来对注册表进行打扫,清理掉曾经作为病毒根据地的场所。到这里,我们的专杀工具(或者我们的简易杀毒软件)就基本上差不多了。当然,大家还得继续往后看,因为我还有后面的内容要继续说。
  特征码的定位好像是很多人想要知道的问题。其实特征码就是从病毒中提取出的特征字节序列。特征码匹配是计算机病毒检测最简单的方法。在DOS下16位的应用程序使用的特征码要取到16位,在Windows下提取的将更长一些,32位、64位或更长些。在特征码中往往会使用“通配符”,因为有些字符需要跳过(跳过的原因或许是因为它不是特征码,或许是因为在每个变种中这些地方的特征码不同)。使用过DOS系统的,对“通配符”这个概念都不会陌生。在DOS下的通配符有“?”、“*”之类的。在病毒特征码中也同样使用通配符,只是它们的含义要在程序中体现出来。
  到此为止,上面提到的基本上都是关于病毒文件的扫描,病毒要执行,那么病毒就肯定会存在于内存。因此,关于内存的扫描对于反病毒软件也是非常重要的。或许,我应该先说内存扫描的问题,然后再讨论关于病毒文件的扫描。好了,还是继续说下去吧。
  内存扫描所涉及到的问题是进程的隔离问题,为了安全起见,Windows是不允许进程A访问进程B的。不过还好,Windows在用户模式下还是为我们提供了读取其他进程的数据的API函数——ReadProcessMemory()。此API通常被调试工具用来控制它所跟踪的程序的执行过程。它需要一个进程句柄作为输入,这个进程句柄可以通过OpenProcess()函数得到。用OpenProcess()打开某些进程得到进程的句柄时必须有相应的权限,调整权限的方法在上文中已经提到过了。用ReadProcessMemory()函数来读取特定应用程序的地址空间,应该知道一个应用程序所使用的页面的准确位置。VirtualQueryEx()函数提供了一个指定进程的虚拟地址空间的页面范围信息。这个函数可以轻松地获得虚拟地址空间的状态(状态包括“已提交状态”、“空闲状态”和“保留状态”三种)。用VirtualQueryEx()函数得到虚拟地址空间状态后,我们可以直接把空闲状态的页面和保留状态的页面忽略掉,而去详细检查“已提交状态”的页面。关于内存扫描的问题,还要继续说吗?以我的能力真的不知道还要说什么了!
  上面的内容也许对付一个普通的用户模式下的病毒勉强有用,不过对于对付内核模式下的病毒可能就真的是半点用都没有了。现在Rootkits可是很风靡的呀!我也凑凑热闹,简单地来说两句吧。我不会说关于Rootkits的技术,而只是简单的列举几个Native API函数供大家了解。
  先说一下进程的遍历。进程的遍历其实有很多方法,相对底层的方法是使用ntdll.dll导出的NtQuerySystemInformation()函数,它是一个Native API(Native API有的地方翻译为本机API,有的翻译为本地API,我觉得那些翻译过的名字叫得不舒服,因此还是写原名比较好)。而这个函数是微软没有公开的函数。NtQuerySystemInformation()函数的原型如下:
  
NTSYSAPI
NTSTATUS
NTAPI
NtQuerySystemInformation(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
OUT PVOID  SystemInformation,
IN ULONG  SystemInformationLength,
OUT PULONG  ReturnLength OPTIONAL );
  
  为什么微软没有公开这个函数,我还能为大家提供这个函数的原型呢?因为这个函数是高手通过逆向后构造出来的。因此这个API函数是无法从MSDN中找到的。这里简单说一下第一个参数,其他的具体解释大家还是到网上找资料看吧。第一个参数如果是5的话(高手对5的定义是这样的:SystemProcessesAndThreadsInformation,它是定义在一个枚举型中的成员变量),表示查询当前运行进程的列表。关于这个函数,我给出一个例子程序!
  
NtQuerySystemInformation(SystemProcessesAndThreadsInformation,pBuffer,cbBuffer,NULL);
PSYSTEM_PROCESSES pInfo=(PSYSTEM_PROCESSES)pBuffer;
for(;;)
{
printf("ProcessID:%d,%ls\n",pInfo->ProcessId,pInfo->ProcessName.Buffer);
if(pInfo->NextEntryDelta==0)
break;
pInfo=(PSYSTEM_PROCESSES)(((PUCHAR)pInfo)+pInfo->NextEntryDelta);
}
  
  遍历进程也许很多人熟悉地是使用PSAPI,而PSAPI的底层实现也是通过NtQureySystemInformation()函数实现的。其实Native API大多都是通过逆向得到的,只要你在调试器中(如OD)对调用API的那句代码单步跟踪进去就会明白了。
  这个NtQuerySystemInformation()函数如果把第一个参数的值改为11的话(高手对11的定义是这样的:SystemModuleInformation),那么它就会返回已经加载的驱动程序基地址的列表。NtQuerySystemInformation()函数的第一个参数在我找到的资料里至少有53种值,真是一个功能强大的函数呀!
  至于其他的内核模式的内存函数就跟前面介绍的那些函数比较类似了,比如NtOpenProcess()、NtTerminateProcess()、NtSuspendThread()和NtQueryVirtualMemory()。
  本文讨论的内容是比较粗浅的,深奥的内容是我能力所不及的。我所讨论的关于内核模式的内容更是非常的少,而且真正的Rootkits中会涉及到很多方面的内容,软硬件方面的内容都有,关于反病毒软件是否能够真正有效地解决Rootkits又是十分重要的。网上很多反Rootkits的软件也比较多,可是很多软件都不能真正地起到作用,但也打着Anti Rootkits软件的旗号来供大家使用。我见过一个Anti Rootkits的软件,只是简单查看一下SSDT是否有被挂钩,如果有被挂钩的话恢复一下SSDT就完事了。真正的反病毒软件或杀毒软件不是一个人就能搞定的,现在也不是一个Linus弄个Linux内核就能当操作系统使用的时代了,毕竟软件的发展已成为产业化了。杀毒软件不是简单的一个扫描文件和扫描内存这么容易的事情,也不是能搞定Rootkits就算是好的杀毒软件。杀毒软件的好坏反映在很多方面,因为杀毒软件是由很多部分组成的,每个部分都是不可忽视的。一个真正的杀毒软件应该至少包括以下的一些个功能模块,比如文件格式识别、壳识别、解压缩、脱壳、特征匹配、未知病毒检测、通用的清除例程……甚至是主动防御。当然,在用户看来,最重要的还是对病毒的识别和清除了。我得个人能力也就这么多了,不足之处还请大家多多包涵!
VC编写简易杀毒软件 免费邮件订阅: 邮件订阅

图片推荐

热点排行榜

CopyRight? 2013 www.cangfengzhe.com All rights reserved