摘自:http://www.shangshuwu.cn/index.php/Linux安全体系的ClamAV病毒扫描程序
ClamAV是使用广泛且基于GPL License的开放源代码的典型杀毒软件,它支持广泛的平台,如:Windows、Linux、Unix等操作系统,并被广泛用于其他应用程序,如:邮件客户端及服务器、HTTP病毒扫描代理等。ClamAV源代码可从http://www.clamav.net 下载。
本章分析了ClamAV的客户端、服务器及病毒库更新升级应用程序,着重阐述了Linux下C语言编程中的许多经典用法。
1 ClamAV概述
计算机防病毒的方法一般有比较法、文件校验和法、病毒扫描法和病毒行为监测法。
病毒比较法有长度比较法、内容比较法、内存比较法、中断比较法等,长度比较法是比较文件的长度是否发生变化,内容比较法是比较文件的内容是否发生变化及文件的更新日期是否改变,内存比较法是正常系统的内存空间是否改变,中断比较法是比较系统的中断向量是否被改变。
病毒比较法常常只能说明系统被改变,至于改变系统的程序是否是病毒以及病毒名都很难确定。
文件校验和法是将正常文件的内容计算其校验和,并将校验和写入写入别的文件保存。以后使用文件时可检查检验和,或定期检查文件校验和,看文件是否发生改变。这种方法只能说明文件的改变,但无法准确地说明是否是病毒。这种方法常被用来保护系统的注册表或系统配置文件。
病毒扫描法(Virus Scanner)是用病毒体中的特定字符串对被检测的文件或内存进行扫描。如果在扫描的文件中病毒的特定字符串,就认为文件感染了字符串所代表的病毒。从病毒中提取的特征字符串被用一定的格式组织在一起并加上签名保护就形成了病毒库。病毒特征字符串或特征字必须能鉴别病毒且必须能将病毒与正常的非病毒程序区分开,因此,对于病毒扫描法来说,病毒特征码的提取很关键,同时,病毒库需要不断的更新,加入新病毒的特征码。
病毒扫描法是反病毒软件最常用的方法,它对已知病毒的扫描非常有效,还能准确的报告病毒的名称,并可以按病毒的特征将病毒从感染的文件中清除。但对未知的病毒却无法检测。
病毒程序还常用被加密或压缩,或放在压缩的软件包中,因此,病毒扫描时还应具备相应的解密和解压缩方法。
病毒行为监测法是根据病毒异常运行行为来判断程序是否感染病毒。这种方法无法准确确认是否是病毒,但可以预报一些未知病毒。
ClamAV是UNIX下的反病毒工具,用于邮件网关的e-mail扫描。它提供了多线程后台,命令行扫描器和通过Internet的自动库升级工具。它还包括一个病毒扫描器共享库。
ClamAV是基于GPL License的开放源代码软件,它支持快速扫描、on-access(文件访问)扫描,可以检测超过35000病毒,包括worms(蠕虫)、trojans(特洛伊木马)、, Microsoft Office和MacOffice宏病毒等。它还可扫描包括Zip、RAR (2.0)、Tar等多种格式的压缩文件,具有强大的邮件扫描器,它还有支持数字签名的先进数据库更新器和基于数据库版本查询的DNS。
ClamAV工具已在GNU/Linux、Solaris、FreeBSD、OpenBSD 2、AIX 4.1/4.2/4.3/5.1HPUX 11.0、SCO UNIX、IRIX 6.5.20f、Mac OS X、BeOS、Cobalt MIPS boxes、Cygwin、Windows Services for Unix 3.5 (Interix)等操作系统平台上经过测试,但有的操作系统中部分特征不支持。
ClamAV包括clamscan查病毒应用程序、clamd后台、clamdscan客户端、libclamav库、clamav-milter邮件扫描器应用程序几个部分。ClamAV工具的组成图如图1。
clamscan查病毒应用程序可直接在命令行下查杀文件或目录的病毒;clamd后台使用libclamav库查找病毒,它的一个线程负责on-access查杀病毒;clamdscan客户端通过clamd后台来查杀病毒,它可以替代clamscan应用程序;libclamav库提供ClamAV接口函数,被其他应用程序调用来查杀病毒;clamav-milter邮件扫描器与sendmail工具连接,使用clamd来查杀email病毒。
ClamAV使用Dazuko软件来进行on-access查杀病毒,Dazuko软件的dazuko内核模块可以使用LSM、系统调用hook和RedirFS模块进行文件访问拦截,Dazuko软件的dazuko库将拦截的文件上报给clamd后台,由clamd来扫描病毒。
图1 ClamAV工具的组成图
2 ClamAV编译安装及使用
编译ClamAV时应包括zlib库,很多程序中的压缩或者解压缩函数都会用到这个库。另外还需要包括bzip2和bzip2-devel库、GNU MP 3库。GMP包允许freshclam验证病毒库的数据签名,你可在http://www.swox.com/gmp/下载GNU MP。
在Linux下编译安装ClamAV的步骤如下:
(1) 下载clamav-0.88.tar.gz
(2) 解压缩文件包
# tar xvzf clamav-0.88.tar.gz
(3)进入解压缩后的clamav目录
# cd clamav-0.88
(4) 添加用户组clamav和组成员clamav
# groupadd clamav # useradd -g clamav -s /bin/false -c "Clam AntiVirus" clamav
(5) 假定你的home目录是/home/gary,如下配置软件:
$ ./ configure --prefix =/ home/ gary/ clamav --disable-clamav
(6) 编译,安装
# make # make install
(7) 在/var/log/目录下添加两个log文件:clam.log和clam-update.log,将所有者改为新加的clamav用户并设置相应的文件读写权限。
( 7 ) 在/ var/ log/ 目录下添加两个log文件:clam.log和clam-update.log,将所有者改为新加的clamav用户并设置相应的文件读写权限。 # touch /var/log/clam-update.log # chmod 600 /var/log/clam-update.log # chown clamav /var/log/clam-update.log # touch /var/log/clam.log # chmod 600 /var/log/clam.log # chown clamav /var/log/clam.log
(8) 修改/etc/clam.conf将开始的有"Example"的那行用#注释掉。
#Example
然后在命令行里输入:clamd开始病毒守护程序。
#clamd
(9) 修改/etc/freshclam.conf将开始的有"Example"的那行用#注释掉。
#Example
修改UpdateLogFile /var/log/freshclam.log
为UpdateLogFile /var/log/clam-update.log
(10) 用freshclam升级病毒库:
#freshclam
(11) 查杀当前目录下的文件
clamscan
(12) 查杀当前目录所有文件及目录!
clamscan -r
(13) 查杀dir目录,
clamscan dir
(14) 查杀目录dir下所有文件及目录!
clamscan -r dir
(15) 看帮助信息
clamscan --help
2.1 clamd后台与clamdscan客户端
clamd是使用libclamav库扫描文件病毒的多线程后台,它可工作在两种网络模式下:侦听在Unix (local) socket和TCP socket。后台由clamd.conf文件配置。通过设置cron的工作,在每隔一段时间检查clamd是否启动运行,并在clamd死亡后自动启动它。在contrib/clamdwatch/目录下有脚本样例。
Clamdscan是一个简单的clamd客户端,许多情况下,你可用它替代clamscan,它仅依赖于clamd,虽然它接受与clamscan同样的命令行选项,但大多数选项被忽略,因为这些选项已在clamd.conf中配置。
clamd的一个重要特征是基于Dazuko模块进行on-access病毒扫描,即拦截文件系统的访问,触发clamd对访问文件进行病毒扫描。Dazuko模块在http://dazuko.org上可用。
clamd中一个名为Clamuko的线程负责与Dazuko进行通信。
Dazuko模块的编译方法如下:
$ tar zxpvf dazuko- a.b .c .tar .gz $ cd dazuko- a.b .c $ make dazuko
或者
$ make dazuko-smp ( 对于smp内核) $ su # insmod dazuko.o # cp dazuko.o /lib/modules/‘uname -r‘/misc # depmod -a
为了Linux启动时会自动加入这个模块,你可以加"dazuko"条目到/etc/modules中,或者在一些启动文件中加入命令modprobe dazuko。
你还必须如下创建一个新设备:
$ cat / proc/ devices | grep dazuko 254 dazuko $ su -c "mknod -m 600 /dev/dazuko c 254 0"
2.2 clamav-milter邮件扫描器
Nigel Horne公司的clamav-milter是Sendmail工具的非常快速的email扫描器。它用C语言编写仅依赖于libclamav或clamd。
通过加入下面的行到/etc/mail/sendmail.mc中,就可将clamav-milter与Sendmail连接起来:
INPUT_MAIL_FILTER( ‘clmilter’,‘S =local :/ var/ run/ clamav/ clmilter.sock, F =, T =S:4m;R:4m’) dnl define( ‘confINPUT_MAIL_FILTERS’, ‘clmilter’)
如果你正以—external运行clamd,检查clamd.conf中的条目是否有如下:
LocalSocket /var/run/clamav/clamd.sock
接着,按下面方法启动clamav-milter:
/usr/local/sbin/clamav-milter -lo /var/run/clamav/clmilter.sock
然后重启动sendmail。
2.3 建立病毒库自动更新
freshclam是ClamAV的缺省数据库更新器,它可以下面两种方法工作:
(1) 交互方式:使用命令行的方式进行交互。
(2) 后台进程的方式:它独立运行不需要干预。
freshclam由超级用户启动,并下降权限,切换到clamav用户。freshclam使用database.clamav.net 轮询调度(round-robin)DNS,自动选择一个数据库镜像。freshclam通过DNS支持数据库版本验证,它还支持代理服务器(与认证一起)、数字签名和出错说明。
ClamAV使用freshclam工具,周期地检查新数据库的发布,并保持数据库的更新。
还可以创建freshclam.log文件,将freshclam.log修改成clamav拥有的log文件,修改方法如下:
# touch /var/log/freshclam.log # chmod 600 /var/log/freshclam.log # chown clamav /var/log/freshclam.log
编辑freshclam.conf文件或clamd.conf文件(如果它们融合在一起),配置UpdateLogFile指向创建的log文件。
以后台运行freshclam的方法如下:
# freshclam –d
还可以使用cron后台自动定时运行freshclam,方法是加入下面行到crontab中:
N * * * * /usr/local/bin/freshclam --quiet
其中,N应是3~57之间的数据,表示每隔N小时检查新病毒数据库。
代理服务器通过配置文件配置,当HTTPProxyPassword被激活时,freshclam需要严格的许可,方法列出如下:
HTTPProxyServer myproxyserver.com
HTTPProxyPort 1234
HTTPProxyUsername myusername
HTTPProxyPassword mypass
配置文件中的DatabaseMirror指定了数据库服务器,freshclam将尝试从这个服务器下载直到最大次数。缺省的数据库镜像是database.clamav.net,为了从最近的镜像下载数据库,你应使用db.xx.clamav.net配置freshclam,xx代表你的国家代码。例如,如果你的服务器在"Ascension Island",你应该加下面的行到freshclam.conf中:
DNSDatabaseInfo current.cvd.clamav.net DatabaseMirror db.ac.clamav.net DatabaseMirror database.clamav.net
两字符国家代码在http://www.iana.org/cctld/cctld-whois.htm上可查找到。
2.4 libclamav库API
每个使用libclamav库的应用程序必须包括clamav.h头文件,方法如下:
#include <clamav.h>
libclamav库API的使用样例见clamscan/manager.c,下面说明API函数。
(1) 装载库
初始化库的函数列出如下:
int cl_loaddb( const char * filename, struct cl_node ** root, unsigned int * signo) ; int cl_loaddbdir( const char * dirname, struct cl_node ** root, unsigned int * signo) ; const char * cl_retdbdir( void ) ;
其中,函数cl_loaddb装载选择的数据库,函数cl_loaddbdir从目录dirname装载所有的数据库,函数返回缺省(硬编码hardcoded)数据库的目录路径。在初始化后,一个内部数据库代表由参数root传出,root必须被初始化到NULL,装载的签名序号由参数signo传出,如果不关心签名计数,参数signo设置为NULL。函数cl_loaddb和cl_loaddbdir装载成功时,返回0,失败时,返回一个负数。
函数cl_loaddb用法如下:
... struct cl_node * root = NULL; int ret, signo = 0 ; ret = cl_loaddbdir( cl_retdbdir( ) , & root, & signo) ;
(2) 错误处理
使用函数cl_strerror将错误代码转换成可读的消息,函数cl_strerror返回一个字符串,使用方法如下:
if ( ret) { //ret是错误码,为负数 printf ( "cl_loaddbdir() error: %s/n " , cl_strerror( ret) ) ; exit( 1 ) ; }
(3) 初始化数据库内部传输
函数cl_build被用来初始化数据库的内部传输路径,函数列出如下:
int cl_build( struct cl_node * root) ;
函数cl_build使用方法如下:
if ( ( ret = cl_build( root) ) ) printf ( "cl_build() error: %s/n " , cl_strerror( ret) ) ;
(4) 数据库重装载
保持内部数据库实例的更新是很重要的,你可以使用函数簇cl_stat来检查数据库的变化,函数簇cl_stat列出如下:
int cl_statinidir( const char * dirname, struct cl_stat * dbstat) ; int cl_statchkdir( const struct cl_stat * dbstat) ; int cl_statfree( struct cl_stat * dbstat) ;
调用函数cl_statinidir初始化结构cl_stat变量,方法如下:
... struct cl_stat dbstat; memset( & dbstat, 0 , sizeof ( struct cl_stat) ) ; cl_statinidir( dbdir, & dbstat) ;
仅需要调用函数cl_statchkdir 来检查数据库的变化,方法如下:
if ( cl_statchkdir( & dbstat) == 1 ) { //数据库发生变化 reload_database...; //重装载数据库 cl_statfree( & dbstat) ; cl_statinidir( cl_retdbdir( ) , & dbstat) ; }
在重装载数据库后,需要重初始化这个结构。
(5) 数据扫描函数
使用下面的函数可以扫描一个buffer、描述符或文件:
int cl_scanbuff( const char * buffer, unsigned int length, const char ** virname, const struct cl_node * root) ; int cl_scandesc( int desc, const char ** virname, unsigned long int * scanned, const struct cl_node * root, const struct cl_limits * limits, unsigned int options) ; int cl_scanfile( const char * filename, const char ** virname, unsigned long int * scanned, const struct cl_node * root, const struct cl_limits * limits, unsigned int options) ;
所有这些函数存储病毒名在指针virname中,它指向内部数据库结构的一个成员,不能直接释放。
后两个函数还支持文件限制结构cl_limits,结构cl_limits用来限制了扫描文件数量、大小等,以防止服务超载攻击,列出如下:
struct cl_limits { int maxreclevel; /* 最大递归级 */ int maxfiles; /*扫描的最大文件数*/ int maxratio; /* 最大压缩率*/ short archivememlim; /* 使用bzip2 (0/1)的最大内存限制*/ long int maxfilesize; /* 最大的文件尺寸,大于这个尺寸的文件不被扫描*/ } ;
参数options配置扫描引擎,并支持下面的标识(可以使用标识组合):
- CL_SCAN_STDOPT 推荐的扫描选项集的别名,它用来给将来libclamav的版本新特征使用。
- CL_SCAN_RAW 不做任何事情,如果不想扫描任何特殊文件,就单独使用它。
- CL_SCAN_ARCHIVE 激活各种文件格式的透明扫描。
- CL_SCAN_BLOCKENCRYPTED 库使用它标识加密文件作为病毒(Encrypted.Zip,Encrypted.RAR)。
- CL_SCAN_BLOCKMAX 如果达到maxfiles、maxfilesize或maxreclevel限制,标识文件作为病毒。
- CL_SCAN_MAIL 激活对邮件文件的支持。
- CL_SCAN_MAILURL 邮件扫描器将下载并扫描列在邮件中的URL,这个标识不应该在装载的服务器上使用,由于潜在的问题,不要在缺省情况下设置这个标识。
- CL_SCAN_OLE2 激活对Microsoft Office文档文件的支持。
- CL_SCAN_PE 激活对便携执行文件(Portable Executable file)的扫描,并允许libclamav解开UPX、Petite和FSG格式压缩的可执行文件。
- CL_SCAN_BLOCKBROKEN libclamav将尝试检测破碎的可执行文件并标识它们为Broken.Executable。
- CL_SCAN_HTML 激活HTML格式(包括Jscript解密)文件扫描。
上面所有函数,如果文件扫描无病毒时,返回0(CL_CLEAN),当检测到立于病毒时,返回CL_VIRUS,函数操作失败返回其它值。
扫描一个文件的方法如下:
... struct cl_limits limits; const char * virname; memset( & limits, 0 , sizeof ( struct cl_limits) ) ; /* 扫描目录中的最大文件数*/ ; limits.maxfiles = 1000 /* 扫描目录中文件最大尺寸*/ limits.maxfilesize = 10 * 1048576 ; /* 10 MB */ /* 最大递归级数*/ limits.maxreclevel = 5 ; /* 最大压缩率*/ limits.maxratio = 200 ; /*取消对bzip2扫描器的内存限制*/ limits.archivememlim = 0 ; if ( ( ret = cl_scanfile( "/home/zolw/test" , & virname, NULL, root, & limits, CL_STDOPT) ) == CL_VIRUS) { printf ( "Detected %s virus./n " , virname) ; } else { printf ( "No virus detected./n " ) ; if ( ret != CL_CLEAN) printf ( "Error: %s/n " , cl_strerror( ret) ) ; }
(6) 释放内存
因为内部数据库的root使用了应用程序分配的内存,因此,如果不再扫描文件时,用下面的函数释放root。
void cl_free( struct cl_node * root) ;
(7) 使用clamav-config命令检查libclamav编译信息
使用clamav-config命令检查的方法及显示的结果列出如下:
# clamav-config --libs -L/ usr/ local/ lib -lz -lbz2 -lgmp -lpthread # clamav-config --cflags -I/ usr/ local/ include -g -O2
(8) ClamAV病毒库格式
ClamAV病毒库(ClamAV Virus Database 简称CVD)是一个数据签名的装有一个或多个数据库的.tar文件。文件头是512字节长字符串,用冒号分开,格式如下:
ClamAV-VDB:build time :version:number of signatures:functionality level required:MD5 checksum:digital signature:builder name:build time ( sec)
使用命令sigtool –info可显示CVD文件的详细信息,方法与显示结果列出如下:
#sigtool -i daily.cvd Build time : 11 Sep 2004 21 -07 +0200 Version: 487 # of signatures: 1189 Functionality level: 2 Builder: ccordes MD5: a3f4f98694229e461f17d2aa254e9a43 Digital signature: uwJS6d+y/ 9g5SXGE0Hh1rXyjZW/ PGK/ zqVtWWVL3/ tfHEnA17z6VB2IBR2I/ OitKRYzm Vo3ibU7bPCJNgi6fPcW1PQwvCunwAswvR0ehrvY/ 4ksUjUOXo1VwQlW7l86HZmiMUSyAjnF/ gciOSsOQa9Hli8D5uET1RDzVpoWu/ idVerification OK
3 clamd服务器
clamd服务器是实现病毒扫描功能的后台进程,它使用socket通信、信号同步、线程池、后台进程等典型技术。
3.1 应用程序命令参数分析
应用程序常使用一些命令行参数选项,应用程序主函数main中常需要对这些参数选项进行分析。参数选项常用"--"和"-"表示,"--"后跟单词表示选项名详解,选项名的值用"="前缀进行标识,"-" 后跟字母表示选项名缩写,选项名的值用空格前缀进行标识。
例如,clamd应用程序的参数选项列出如下:
$clamd –help Clam AntiVirus Daemon 0.88.4 ( C) 2002 - 2005 ClamAV Team - http: //www.clamav.net/team.html -- help - h Show this help. -- version - V Show version number. -- debug Enable debug mode. -- config- file= FILE - c FILE Read configuration from FILE.
标准C库提供了下述函数进行命令行参数分析:
#include <unistd.h> int getopt( int argc, char * const argv[ ] , const char * optstring) ; extern char * optarg; //用于存放选项值 extern int optind, opterr, optopt; #define _GNU_SOURCE #include <getopt.h> int getopt_long( int argc, char * const argv[ ] , const char * optstring, const struct option * longopts, int * longindex) ; int getopt_long_only( int argc, char * const argv[ ] , const char * optstring, const struct option * longopts, int * longindex) ;
其中,函数参数argc和argv是main函数的参数,参数optstring表示分析选项的方法,参数longopts为用户定义的选项数组,longindex为命令行选项的序号。
函数getopt可以分析"-"标识的命令行参数,函数getopt_long是对getopt的扩展,它可以分析"-"和"--"标识的命令行参数。命令行参数解析时,函数每解析完一个argv,optind加1,返回该选项字符。当解析完成时,返回-1。
参数optstring一般由用户设置,表示分析命令行参数的方法,参数optstring说明如下:
optstring为类似"a:b"字符串表示命令行参数带有选项值。
optstring为类似"a::b"字符串表示命令行参数可能带有选项值,也可能没有。
optstring的开头字符为":",表示如果选项值失去命令行参数时,返回":",而不是" '",默认时返回" '"。
optstring的开头字符为'+',表示遇到无选项参数,马上停止扫描,随后的部分当作参数来解释。
optstring的开头字符为'-',表示遇到无选项参数,把它当作选项1的参数。
结构option描述了命令行一个参数选项的构成,结构option说明如下:
struct option { const char * name; //长参数选项名 int has_arg; //选项值个数:0,1,2,为2时表示值可有可无 int * flag; // flag为NULL,则getopt_long返回val。否则返回0 int val; //指明返回的值,短参数名 } ;
clamd服务器是一个后台进程,它实现了病毒扫描的具体功能。在clamd/options.c中的函数main解析了命令行的各种选项,函数main调用C库函数getopt_long依次分析出每个命令行选项,并将每个命令行选项及值存储在链表中。链表定义如下(在clamd/options.h中):
struct optnode { //链表结点结构 char optchar; //短选项名 char * optarg; //选项值,来自于C库函数getopt_long解析并存在全局变量optarg中的选项值 char * optname; //长选项名 struct optnode * next; //下一个结点,当为最后一个结点时,指向NULL } ; struct optstruct { struct optnode * optlist; //命令行选项的链表 char * filename; } ;
clamd服务器在clamd/options.c中提供了对这个链表的操作函数,如:创建链表、释放链表、读取链表成员、加入链表成员等。
函数main选定了应用程序所支持的命令行参数选项数组long_options[],函数getopt_long解析命令行选项时,如果长选项对应匹配有短选项,将输出短选项,如:"--configfile"对应"-c"。解析出的选项存在链表opt->optlist中,非参数选项字符存在opt->filename中。
例如,当输入clamd --config-file=test test1 -V --debug test2时,opt->optlist链表中将存有{0,0,debug},{‘V’,0,0},{‘c’,"test",0},opt->filename存有"test1 test2"。
函数main 列出如下(在clamd/options.c中):
int main( int argc, char ** argv) { int ret, opt_index, i, len; struct optstruct * opt; const char * getopt_parameters = "hc:V" ; //支持的短选项名集合 //应用程序clamd支持的参数选项定义 static struct option long_options[ ] = { { "help" , 0 , 0 , 'h' } , { "config-file" , 1 , 0 , 'c' } , { "version" , 0 , 0 , 'V' } , { "debug" , 0 , 0 , 0 } , { 0 , 0 , 0 , 0 } } ; ...... opt = ( struct optstruct* ) mcalloc( 1 , sizeof ( struct optstruct) ) ; //创建参数链表 opt-> optlist = NULL; opt-> filename = NULL; while ( 1 ) { //循环解析每个命令行选项 opt_index= 0 ; //解析一个命令行选项,解析出的值存在于C库全局变量optarg中,匹配的短选项名存于ret中 // getopt_parameters为格式"hc:V",argv将重排序,非选项参数依次排在选项参数之后 ret= getopt_long( argc, argv, getopt_parameters, long_options, & opt_index) ; if ( ret == - 1 ) //选项解析完毕,跳出循环 break ; switch ( ret) { case 0 : //ret为0,表示没有匹配的短选项名,如:"debug" register_long_option( opt, long_options[ opt_index] .name ) ; //将无配置短选项名的长选项存入链表 break ; default : if ( strchr( getopt_parameters, ret) ) //ret是否属于支持的短选项名 register_char_option( opt, ret) ; //短选项存入链表 else { fprintf( stderr, "ERROR: Unknown option passed./n " ) ; free_opt( opt) ; exit( 40 ) ; } } } if ( optind < argc) { len= 0 ; /* 计数非选项参数长度 */ for ( i= optind; i< argc; i++ ) //选项参数optind之后为非选项参数 len+= strlen( argv[ i] ) ; //计算非选项参数的长度,如:test1,test2 len= len+ argc- optind- 1 ; /* add spaces between arguments */ opt-> filename= ( char * ) mcalloc( len + 256 , sizeof ( char ) ) ; //存入非选项参数,opt->filename =“test1 test2” for ( i= optind; i< argc; i++ ) { strncat( opt-> filename, argv[ i] , strlen( argv[ i] ) ) ; //连接字符串 if ( i != argc- 1 ) strncat( opt-> filename, " " , 1 ) ; //连接一个空格 } } clamd( opt) ; free_opt( opt) ; //释放链表 return ( 0 ) ; }
函数register_long_option将命令行的长选项添加到链表中,函数register_long_option列出如下(在clamd/options.c中):
void register_long_option( struct optstruct * opt, const char * optname) { struct optnode * newnode; newnode = ( struct optnode * ) mmalloc( sizeof ( struct optnode) ) ; //分配新结点 newnode-> optchar = 0 ; if ( optarg != NULL) { // optarg是C库的全局变量,存储有函数getopt_long分析出的选项值 newnode-> optarg = ( char * ) mcalloc( strlen( optarg) + 1 , sizeof ( char ) ) ; strcpy( newnode-> optarg, optarg) ; //拷贝选项参数值 } else newnode-> optarg = NULL; //分配字符串空间,加1是为了字符串结尾保护 newnode-> optname = ( char * ) mcalloc( strlen( optname) + 1 , sizeof ( char ) ) ; strcpy( newnode-> optname, optname) ; //拷贝选项名 newnode-> next = opt-> optlist; //新结点加入到链表 opt-> optlist = newnode; }
函数clamd根据命令行选项值调用相应处理函数,函数clamd中与命令行选项处理相关的代码列出如下:
void clamd( struct optstruct * opt) { ...... if ( optc( opt, 'V' ) ) { //检查命令行选项链表中是否含有短选项名为'V'的成员 print_version( ) ; //打印版本信息 exit( 0 ) ; } if ( optc( opt, 'h' ) ) { //检查命令行选项链表中是否含有短选项名为'h'的成员 help( ) ; //打印帮助信息 } if ( optl( opt, "debug" ) ) { //检查命令行选项链表中是否含有长选项名为"debug"的成员 ...... debug_mode = 1 ; //设置为调试模式 } ...... }
3.2 clamd服务器入口函数clamd
函数clamd是clamd服务器的入口函数。clamd服务器在函数main解析了命令行的各种选项后,调用函数clamd。clamd服务器由函数clamd建立,函数clamd分析配置文件后,调用umask(0)使进程具有读写执行权限,然后初始化logger系统(包括使用syslog),并通过设置组ID和用户ID来降低权限,还设置临时目录的环境变量,装载病毒库,使进程后台化。然后,使用socket套接口,接收客户端的服务请求并启动相应的服务。
函数clamd列出如下(在clamav/clamd.c中):
void clamd( struct optstruct * opt) { ...... /* 省略与根据命令行选项打印病毒库版本、帮助信息*/ ...... if ( optl( opt, "debug" ) ) { //如果有debug选项,设置debug标识 #if defined(C_LINUX) struct rlimit rlim; rlim.rlim_cur = rlim.rlim_max = RLIM_INFINITY; //表示core文件大小不受限制,core文件是应用崩溃时记录的内存映像 if ( setrlimit( RLIMIT_CORE, & rlim) < 0 ) perror( "setrlimit" ) ; #endif debug_mode = 1 ; //debug标识,debug_mode是全局变量 } /* 分析配置文件 */ if ( optc( opt, 'c' ) ) cfgfile = getargc( opt, 'c' ) ; else cfgfile = CL_DEFAULT_CFG; if ( ( copt = parsecfg( cfgfile, 1 ) ) == NULL) { fprintf( stderr, "ERROR: Can't open/parse the config file %s/n " , cfgfile) ; exit( 1 ) ; } //加权限掩码,即掩码为1的权限位不起作用,掩码为0,表示为777权限,即root权限 umask( 0 ) ; /*初始化logger */ ...... if ( ( cpt = cfgopt( copt, "LogFile" ) ) ) { logg_file = cpt-> strarg; ...... time ( & currtime) ; //得到当前时间 if ( logg( "+++ Started at %s" , ctime( & currtime) ) ) { //将当前时间写入log文件 fprintf( stderr, "ERROR: Problem with internal logger. Please check the permissions on the %s file./n " , logg_file) ; //将错误写出到标准错误输出句柄上,一般为标准输出 exit( 1 ) ; } } else logg_file = NULL; #在系统log中加入clamd已启动信息 #if defined(USE_SYSLOG) && !defined(C_AIX) if ( cfgopt( copt, "LogSyslog" ) ) { int fac = LOG_LOCAL6; //表示是本地消息 if ( ( cpt = cfgopt( copt, "LogFacility" ) ) ) { if ( ( fac = logg_facility( cpt-> strarg) ) == - 1 ) { fprintf( stderr, "ERROR: LogFacility: %s: No such facility./n " , cpt-> strarg) ; exit( 1 ) ; } } openlog( "clamd" , LOG_PID, fac) ; // LOG_PID表示每条消息中加入pid logg_syslog = 1 ; syslog( LOG_INFO, "Daemon started./n " ) ; //将字符串加入到系统log,表示clamd已启动 } #endif ...... #ifdef C_LINUX procdev = 0 ; if ( stat( "/proc" , & sb) != - 1 && ! sb.st_size ) procdev = sb.st_dev ; #endif ...... /* 通过设置组ID和用户ID来降低权限*/ ...... /* 设置临时目录的环境变量*/ if ( ( cpt = cfgopt( copt, "TemporaryDirectory" ) ) ) cl_settempdir( cpt-> strarg, 0 ) ; if ( cfgopt( copt, "LeaveTemporaryFiles" ) ) cl_settempdir( NULL, 1 ) ; /* 装载病毒库*/ if ( ( cpt = cfgopt( copt, "DatabaseDirectory" ) ) || ( cpt = cfgopt( copt, "DataDirectory" ) ) ) dbdir = cpt-> strarg; else dbdir = cl_retdbdir( ) ; //从DATADIR得到缺省病毒库目录 if ( ( ret = cl_loaddbdir( dbdir, & root, & virnum) ) ) { //装载病毒库 ...... } ...... if ( ( ret = cl_build( root) ) != 0 ) { ...... } /* fork进程后台*/ if ( ! cfgopt( copt, "Foreground" ) ) daemonize( ) ; if ( tcpsock) ret = tcpserver( opt, copt, root) ; else ret = localserver( opt, copt, root) ; logg_close( ) ; freecfg( copt) ; }
3.3 设置系统限制及确定资源使用量
C库中与资源限制相关的函数有函数getrlimit和setrlimit,列出如下:
#include <sys/types.h> #include <sys/time.h> #include <sys/resource.h> //得到资源种类resource的限制值(包括当前限制值和最大限制值) int getrlimit( int resource, struct rlimit * rlp) ; // resource表示资源种类,rlp设置资源限制值 int setrlimit( int resource, const struct rlimit * rlp) ; //设置限制值 //参数who为或RUSAGE_CHILDREN,RUSAGE_SELF表示得到当前进程的资源使用信息,RUSAGE_CHILDREN表示得到子进程的资源使用信息 //参数rusage用于存储查询到的资源使用信息 int getrusage( int who, struct rusage * rusage) ;
函数getrlimit查询本进程所受的系统限制,系统的限制通过结构rlimit来描述,结构rlimit列出如下:
struct rlimit { rlim_t rlim_cur; //当前的限制值 rlim_t rlim_max; //最大限制值 } ;
在结构rlimit中,rlim_cur表示进程的当前限制,它是软限制,进程超过软限制时,还继续运行,但会收到与当前限制相关的信号。rlim_max是进程的最大限制,仅由root用户设置,进程不能超过它的最大限制,当前限制可以最大限制的范围内设置。
资源类型的宏定义在/usr/include/bits/resource.h文件中,列出如下:
/* 指示没有限制的值*/ #ifndef __USE_FILE_OFFSET64 # define RLIM_INFINITY #else # define RLIM_INFINITY 0xffffffffffffffffuLL #endif enum __rlimit_resource { /* 本进程可以使用CPU的时间秒数,达到当前限制,收到SIGXCPU信号*/ RLIMIT_CPU = 0 , #define RLIMIT_CPU RLIMIT_CPU /*本进程可创建的最大文件尺寸,超出限制时,发出SIGFSZ信号*/ RLIMIT_FSIZE = 1 , #define RLIMIT_FSIZE RLIMIT_FSIZE /*进程数据段的最大尺寸,数据段是C/C++中用malloc()分配的内存,超出限制,将不能分配内存*/ RLIMIT_DATA = 2 , #define RLIMIT_DATA RLIMIT_DATA /*进程栈的最大尺寸,超出限制,会收到SIGSEV信号*/ RLIMIT_STACK = 3 , #define RLIMIT_STACK RLIMIT_STACK /* 进程能创建的最大core文件,core文件用于进程崩溃时倒出进程现场,达到限制时,将中断写core文件的进程*/ RLIMIT_CORE = 4 , #define RLIMIT_CORE RLIMIT_CORE /* 最大常驻集(resident set)尺寸,它影响到内存交换,超出常驻集尺寸的进程将可能被释放物理内存*/ RLIMIT_RSS = 5 , #define RLIMIT_RSS RLIMIT_RSS /*进程打开文件的最大数量,超出限制,将不能打开文件*/ RLIMIT_NOFILE = 7 , RLIMIT_OFILE = RLIMIT_NOFILE, /* 用于BSD操作系统*/ #define RLIMIT_NOFILE RLIMIT_NOFILE #define RLIMIT_OFILE RLIMIT_OFILE /*地址空间限制*/ RLIMIT_AS = 9 , #define RLIMIT_AS RLIMIT_AS /*应用程序可以同时开启的最大进程数*/ RLIMIT_NPROC = 6 , #define RLIMIT_NPROC RLIMIT_NPROC /* 锁住在内存的地址空间*/ RLIMIT_MEMLOCK = 8 , #define RLIMIT_MEMLOCK RLIMIT_MEMLOCK /* 文件锁的最大数量*/ RLIMIT_LOCKS = 10 , #define RLIMIT_LOCKS RLIMIT_LOCKS RLIMIT_NLIMITS = 11 , RLIM_NLIMITS = RLIMIT_NLIMITS #define RLIMIT_NLIMITS RLIMIT_NLIMITS #define RLIM_NLIMITS RLIM_NLIMITS } ;
当将rlim_cur设置RLIM_INFINITY时,进程将不收到任何限制警告。如果进程不愿处理当前限制引起的信号,可将rlim_cur设置RLIM_INFINITY。
函数clamd 中调用到setrlimit(RLIMIT_CORE, &rlim),它将不限制core文件的大小,core文件用于在进程崩溃时,倒出(dump)进程的现场,core文件存储这个现场信息用于对进程崩溃的调试分析。
3.4 配置文件解析
clamd服务器的配置文件在clamav/etc/clamd.conf文件中,应用程序的配置文件是由用户直接写入的文本文件。应用程序分析配置文件,得到用户对应用程序运行设置的选项。应用程序分析配置文件就会用到配置文件分析器。配置文件分析器是分析配置文件的函数,包括函数parsecfg、freecfg、regcfg和cfgopt。函数parsecfg分析配置文件,调用函数regcfg将分析出的配置项及值存入配置链表中;函数cfgopt从配置链表中查询配置项名,返回配置项名对应的配置。
配置选项由配置选项名和选项值组成,配置文件中的配置选项类型使用结构cfgoption描述。配置文件分析器分析配置文件每行后,得到选项名与选项值,选项值可能为数字或字符。选项名及选项值存在结构cfgstruct。
函数parsecfg分析配置文件后,将配置选项组成一个结构cfgstruct实例的链表返回。
结构cfgoption和cfgstruct列出如下(在clamav/shared/cfgparser.h中):
struct cfgoption { const char * name; //选项名 int argtype; //选项的类型 } ; struct cfgstruct { char * optname; //选项名 char * strarg; //字符串选项值 int numarg; //数字选项值 struct cfgstruct * nextarg; struct cfgstruct * next; } ;
函数*parsecfg从配置文件clamd.conf文件中读取每行,每行由选项名和选项参数值组成。将配置文件每行的选项名与服务器应用程序中选项数组cfg_options中选项进行比较,若相匹配,根据选项类型分析选项参数值,并将选项名与选项值加入到选项链表的节点上。函数*parsecfg返回由配置文件生成的选项链表。
函数*parsecfg列出如下(在clamav/shared/cfgparser.c中):
struct cfgstruct * parsecfg( const char * cfgfile, int messages) { ...... //定义配置文件中选项的类型 struct cfgoption cfg_options[ ] = { { "LogFile" , OPT_FULLSTR} , //占一行的字符串参数 { "LogFileUnlock" , OPT_NOARG} , //没有参数 { "LogFileMaxSize" , OPT_COMPSIZE} , //转换KByte和MByte到Byte ...... { "OnUpdateExecute" , OPT_FULLSTR} , /* freshclam */ { "OnErrorExecute" , OPT_FULLSTR} , /* freshclam */ { "OnOutdatedExecute" , OPT_FULLSTR} , /* freshclam */ { "LocalIPAddress" , OPT_STR} , /*用于freshclam的字符串参数*/ { 0 , 0 } , //表示结尾 } ; if ( ( fs = fopen( cfgfile, "r" ) ) == NULL) //打开配置文件 return NULL; /*函数fgets从fs中读取尺寸最大LINE_LENGTH(为1024)长度的字符,存入buff中,当读到EOF时,读操作停止。下次再调用函数fgets时,将从新的一行开始。读完一行后,在buff末尾加上‘/0’字符*/ while ( fgets( buff, LINE_LENGTH, fs) ) { //每次读取配置文件的一行 line++; if ( buff[ 0 ] == '#' ) //跳过注释行 continue ; //如果存在Example行,说明配置文件还没修改过,需要用户配置好后,去掉这一行,这样配置文件才可用 if ( ! strncmp( "Example" , buff, 7 ) ) { //如果buff的前7个字符为“Example” if ( messages) fprintf( stderr, "ERROR: Please edit the example config file %s./n " , cfgfile) ; fclose( fs) ; return NULL; } //每行的第一个域为选项名,第二个域为参数 if ( ( name = cli_strtok( buff, 0 , " /r /n " ) ) ) { //得到一行的第0域的值,每个域以“/r/n”中的字符分隔 arg = cli_strtok( buff, 1 , " /r /n " ) ; found = 0 ; //遍历选项数组cfg_options,查找与配置文件相匹配的选项名 for ( i = 0 ; ; i++ ) { pt = & cfg_options[ i] ; if ( pt-> name) { if ( ! strcmp( name, pt-> name) ) { //在数组中找到与配置文件中相匹配的选项 found = 1 ; switch ( pt-> argtype) { //根据选项类型分析字符串 case OPT_STR: //字符串参数 ...... copt = regcfg( copt, name, arg, 0 ) ; break ; case OPT_FULLSTR: //占一行的字符串参数 ...... free ( arg) ; //返回buff匹配子字符串" "(空格)的位置的字符指针 arg = strstr( buff, " " ) ; arg = strdup( ++ arg) ; //strdup表示字符复制,这里用于删除空格 //如果“/n/r”任何一个字符在arg中,返回匹配位置开始字符指针 if ( ( c = strpbrk( arg, "/n /r " ) ) ) //将“/n/r”转换成“/0” * c = '/0 ' ; copt = regcfg( copt, name, arg, 0 ) ; break ; case OPT_NUM: //数字参数 ...... copt = regcfg( copt, name, NULL, atoi( arg) ) ; //将字符转换成int类型 free( arg) ; break ; case OPT_COMPSIZE: //转换KByte和MByte到Byte ...... //将arg的最后一个字符转换成小写 ctype = tolower( arg[ strlen( arg) - 1 ] ) ; if ( ctype == 'm' || ctype == 'k' ) { char * cpy = ( char * ) mcalloc( strlen( arg) , sizeof ( char ) ) ; //拷贝参数arg除表示单位的字符外的字符串到cpy strncpy( cpy, arg, strlen( arg)