编写简易杀毒软件
文/ 冀云[太原铁通分公司]
杀毒软件的编写,也许一直以来都是高手手中的法宝。我不是高手,也没有能力写一款杀毒软件出来。我的文章也没有新颖之处,说是“简易”,其实类似于大家编写的专杀工具,或者是一些国内个人防毒软件使用的技术。我只是简单地整理了一些大家在编写专杀时常常能使用到的代码,或许对于大家在写专杀或者写个人防毒软件时能用得着,毕竟善于总结和归纳也是一种学习的方法和提高的手段。
我不知道编写一个防毒工具或专杀工具应该从哪个地方开始入手,也不知道要从哪部分开始介绍。算了,还是不去想先后顺序了,一部分一部分地介绍吧,只要能把我想说的都说完就可以了。
首先从文件扫描开始吧。文件扫描是每个杀毒软件都有的,不管是“扫描全盘”也好,还是针对某个分区或是文件夹扫描也好,都要进行文件扫描才能完成查找病毒的任务。这个问题并不怎么难理解,只要是使用过杀毒软件或者使用过搜索文件工具的人都应该非常清楚。好吧,既然大家都了解了,那么就看看具体实现扫描的代码的编写吧。
DWORD WINAPI FindFiles(LPVOID lpszPath)
{……//此处省略了很多变量的定义
szFilter="*.*"; //定义扫描文件的类型,这里是所有文件
lstrcpy(szPath,(char *)lpszPath);
len=lstrlen(szPath);
if(szPath[len-1]!=\)
{
szPath[len]=\;
szPath[len+1]=;
}
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
",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()函数就可以了。
最后,我
补充:综合编程 , 安全编程 ,