/* $Id: avcheck.c,v 1.21 2002/10/09 22:07:59 mjt Exp $ * avcheck mail virus scanner client. * Michael Tokarev * Public domain. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef INADDR_NONE /* some systems have no INADDR_NONE macro */ # define INADDR_NONE ((unsigned long) -1) #endif #ifndef PARANOID # define PARANOID 1 #endif #ifndef AVP # define AVP 1 #endif #ifndef DRWEB # define DRWEB 1 #endif #ifndef SOPHIE # define SOPHIE 0 /* sophie can't handle MIME properly */ #endif #ifndef TROPHIE # define TROPHIE 0 /* trophie can't handle MIME */ #endif static char *progname; static char *path; /* temporary file name */ #define MAXARGS 16 static char *sendmail_argv[MAXARGS]; static int sendmail_argc; static char buf[8192]; /* i/o buffer */ static void cleanup() { if (path) unlink(path); } static void cleanup_child() { /* we don't want to delete these */ path = NULL; } static void err(int errcode, const char *fmt, ...) { va_list ap; fprintf(stderr, "%s: ", progname); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); if (errcode) fprintf(stderr, ": %s", strerror(errcode)); putc('\n', stderr); fflush(stderr); cleanup(); exit(EX_TEMPFAIL); } static void warn(const char *fmt, ...) { va_list ap; #ifndef LOG_PERROR # define LOG_PERROR 0 #endif openlog(progname, LOG_PID|LOG_PERROR, LOG_MAIL); va_start(ap, fmt); vsyslog(LOG_WARNING, fmt, ap); va_end(ap); } static void timeout(int unused_sig) { err(0, "timeout waiting for antivirus daemon"); } static void sighandler(int sig) { err(0, "got fatal signal %d", sig); } static int iwrite(int fd, const char *buf, int len) { do { int r = write(fd, buf, len); if (r < 0) return r; len -= r; buf += r; } while(len); return 0; } #ifdef QUOTEADDR /* determine if a given localpart (between s and e) needs to be quoted */ static int is_rfc821_ok(const char *s, const char *e) { if (s == e) return 1; if (!*s || *s == '.') return 0; while(s < e) { if ((*s & 128) || *s <= ' ' || strchr("<>()[]\\,;:\"", *s) || (s[0] == '.' && s[1] == '.')) return 0; ++s; } return 1; } /* quote address if needed, allocating memory if alloc is true */ #define quoteaddr(addr) ((const char*)_quoteaddr((addr),0)) #define quoteaddr_alloc(addr) (_quoteaddr((addr),1)) static char * _quoteaddr(const char *addr, int alloc) { int l, r; const char *e = strrchr(addr, '@'); if (!e) e = addr + strlen(addr); if (is_rfc821_ok(addr, e)) return (char*)addr; l = 0; buf[l++] = '"'; r = sizeof(buf) - strlen(e) - 2; while(addr < e) { if (l > r) /* nothing we can do here */ /*XXX err(EX_UNAVAILABLE, 0, "address is too long");*/ err(0, "address is too long"); if ((*addr & 128) || strchr("\r\n\"\\", *addr)) buf[l++] = '\\'; buf[l++] = *addr++; } buf[l++] = '"'; strcpy(buf + l, e); if (alloc) { char *b = strdup(buf); if (!b) err(0, "unable to allocate memory"); return b; } else return buf; } #else /* !QUOTEADDR */ # define quoteaddr(addr) (addr) # define quoteaddr_alloc(addr) (addr) #endif /* read SMTP dialog responce from opened filedescriptor fd, expecting exp code */ static void smtpresp(int fd, int exp) { #ifdef DEBUG fprintf(stderr, "(expecting SMTP response %d)\n", exp); #else int l; char *p; l = read(fd, buf, sizeof(buf)); if (l < 3) err(l < 0 ? errno : 0, "unable to read smtp response"); if (buf[--l] != '\n') err(0, "long line in smtp response"); if (buf[l-1] == '\r') --l; buf[l] = '\0'; l = 0; for(p = buf; p < buf+3; ++p) if (*p >= '0' && *p <= '9') l = l * 10 + (*p - '0'); else break; if (*p != ' ' || l != exp) err(0, "unexpected smtp response (need %d): %s", exp, buf); #endif } /* issue printf-style SMTP command and wait for response, expecting resp */ static void smtpcmd(FILE *f, int resp, const char *fmt, const char *arg) { if (fprintf(f, fmt, arg) < 0 || fputs("\r\n", f) < 0 || fflush(f) < 0) err(errno, "unable to write smtp command"); smtpresp(fileno(f), resp); } /* create and connect TCP socket based on spec. defport is a default port number to use, what used for error messages if any */ static int tcpsock(const char *spec, int defport, const char *what) { int fd; char *h, *p; struct sockaddr_in ssin; if ((p = strchr(spec, ':')) != NULL) { int len = p - spec; h = (char*)alloca(len + 1); memcpy(h, spec, len); h[len] = '\0'; ++p; } else h = (char*)spec; memset(&ssin, 0, sizeof(ssin)); ssin.sin_family = AF_INET; if (!*h) h = "127.0.0.1"; if ((ssin.sin_addr.s_addr = inet_addr(h)) == INADDR_NONE || ssin.sin_addr.s_addr == 0) { struct hostent *he = gethostbyname(h); if (!he) err(0, "host not found: %s", h); if (he->h_addrtype != AF_INET) err(0, "unexpected address family for %s", h); if (he->h_length != sizeof(ssin.sin_addr)) err(0, "unexpected address length for %s", h); memcpy(&ssin.sin_addr, he->h_addr, sizeof(ssin.sin_addr)); } if (!p || !*p) ssin.sin_port = htons(defport); else if ((defport = atoi(p)) > 0) ssin.sin_port = htons(defport); else { struct servent *se = getservbyname(p, "tcp"); if (!se) err(0, "unknown service %s", p); ssin.sin_port = se->s_port; } fd = socket(AF_INET, SOCK_STREAM, 0); if (fd < 0) err(errno, "unable to create socket"); if (connect(fd, (struct sockaddr *)&ssin, sizeof(ssin)) < 0) err(errno, "unable to connect to %s", what); return fd; } /* send a file using external command or smtp-"lite" protocol */ /* If translate is non-zero, then we should "translate" any external command error code into EX_TEMPFAIL, thus requiring to fork/exec and pipe, waiting to completion */ static void sendit(int msgfd, char *from, char **to, int nto, int translate, char *header) { static const char *htemplate = "X-AV-Checked: %.24s %s%s"; /* open mail stream */ if (sendmail_argv[0][0] != '/') { /* do speak smtp */ int nl; char *bp, *be; #ifdef DEBUG int fd = 2; FILE *f = stderr; fprintf(stderr, "faking TCP connection to %s\n", sendmail); #else int fd = tcpsock(sendmail_argv[0], 25, "smtp server"); FILE *f = fdopen(fd, "w"); if (!f) err(errno, "unable to create smtp stream"); #endif smtpresp(fd, 220); smtpcmd(f, 250, "HELO localhost", NULL); smtpcmd(f, 250, "MAIL FROM:<%s>", quoteaddr(from)); for(nl = 0; nl < nto; ++nl) smtpcmd(f, 250, "RCPT TO:<%s>", quoteaddr(to[nl])); smtpcmd(f, 354, "DATA", NULL); /* copy mail file into smtp stream */ if (header) { time_t now = time(NULL); fprintf(f, htemplate, ctime(&now), header, "\r\n"); } /* nl = 1 means we're at a start of a line (watch for dots!) */ for (nl = 1, bp = be = buf;;) { if (bp == be) { int l = read(msgfd, buf, sizeof(buf)); if (l < 0) err(errno, "unable to read"); if (!l) break; bp = buf; be = buf + l; } if (*bp == '\n') { putc('\r', f); nl = 1; } else if (nl) { if (*bp == '.') putc('.', f); nl = 0; } if (putc(*bp, f) < 0) break; ++bp; } if (!nl) fputs("\r\n", f); /* finish smtp stream */ if (fputs(".\r\n", f) < 0 || ferror(f) || fflush(f) < 0) err(errno, "unable to write smtp stream"); smtpresp(fileno(f), 250); write(fileno(f), "QUIT\r\n", 6); #ifndef DEBUG read(fileno(f), buf, sizeof(buf)); /* ignore any response */ fclose(f); #endif } else { /* execute sendmail program, talk via pipe if translate is non-zero, or execute it directly replacing our process */ int fd, pfd[2]; int l; pid_t cpid = 0; /* XXX note: header!=NULL can only happen with translate==true */ if (translate && (pipe(pfd) != 0 || (cpid = fork()) < 0)) err(errno, "unable to pipe() or fork()"); if (!translate || cpid == 0) { /* child */ char **av = (char**)alloca(sizeof(char*) * (sendmail_argc + nto + 4)); int ac; cleanup_child(); close(pfd[1]); if (pfd[0] != 0) { if (dup2(pfd[0], 0) < 0) err(errno, "unable to redirect input"); close(pfd[0]); } for(ac = 0; ac < sendmail_argc; ++ac) av[ac] = sendmail_argv[ac]; av[ac++] = "-f"; av[ac++] = quoteaddr_alloc(from); av[ac++] = "--"; for(l = 0; l < nto; ++l) av[ac++] = quoteaddr_alloc(to[l]); av[ac] = NULL; #ifdef DEBUG fprintf(stderr, "faking execution of:\n"); for(l = 0; av[l]; ++l) fprintf(stderr, " %s", av[l]); putc('\n', stderr); while((l = read(0, buf, sizeof(buf))) > 0) write(2, buf, l); exit(0); #else execvp(sendmail_argv[0], av); err(errno, "unable to execute %s", sendmail_argv[0]); #endif } close(pfd[0]); fd = pfd[1]; if (header) { time_t now = time(NULL); snprintf(buf, sizeof(buf) - 1, htemplate, ctime(&now), ""); l = strlen(buf); buf[l++] = '\n'; /* no overflow: \0 at the end */ if (write(msgfd, header, l) != l) err(errno, "unable to write header to mail pipe"); } while((l = read(msgfd, buf, sizeof(buf))) > 0) if (write(fd, buf, l) != l) err(errno, "unable to write to mail pipe"); if (l < 0) err(errno, "unable to read input"); if (close(fd) < 0 || wait(&l) < 0) err(errno, "unable to finish mail pipe"); if (l != 0) err(0, "unable to send mail: %s returned %d", sendmail_argv[0], l); } } #if AVP static int scan_avp(int fd, const char *path, const char *avname) { unsigned short r; /*XXX size/endiannes problem */ char *p, *t, *n; int l, len; time_t now = time(NULL); l = 3 + 15 + 1 + strlen(path); p = (char*)alloca(l + 1); sprintf(p, "<0>%.15s:%s", ctime(&now) + 4, path); errno = 0; if (iwrite(fd, p, l) < 0) err(errno, "unable to send command to %s", avname); if (read(fd, &r, 2) != 2) /*XXX endiannes problem */ err(errno, "unable to read %s responce", avname); /*XXX avp uses very bogus protocol. Here we're ignoring it's text answer(s) unless code says that it found some virus(es). */ switch(r & 0xcf) { case 0: /* the only one OK result. */ /*XXX unfortunately avp is so buggy that it will report success if it encountered some error(s), so that this is not really "OK result." */ return 0; case 1: /* virus scan was incomplete */ /* this returned when AVP is unable to parse file structure for various reasons. */ case 8: /* file is corrupted (e.g. archive) - do not treat it as bad */ warn("unexpected %s return code %d (0x%04x) (%s)", avname, r & 0xcf, r, ((r & 0xcf) == 1) ? "virus scan was incomplete" : "file is corrupted"); return 0; case 4: /* known virus(es) detected */ case 3: /* suspicious object(s) found */ case 2: /* Found corrupted or changed virus (usually false positive, controlled by Warnings=Yes|No in defUnix.prf) */ break; default: /* some more additional codes to help diagnose problem(s) */ if ((r & 0xcf) == 7) p = "kavdaemon is corrupted"; else if ((r & 0xcf) == 0x0f) p = "kavdaemon wants confirmation -- set InfectedAction to 0"; else if (r & 0x80) p = "kavdaemon integrity failed"; else if (r & 0x40) p = "kavdaemon av bases not found"; else if (r & 0x10) p = "kavdaemon key file not found or expired"; else err(0, "unexpected %s return code %d (0x%04x)", avname, r & 0xcf, r); err(0, "unexpected %s return code %d (0x%04x) (%s)", avname, r & 0xcf, r, p); } /* here we're if infected or suspicious object(s) was found by avp */ /* since we will work here with only one file (message), we will not deal with memory deallocation for msg and *msgp. */ if (!(r & 0x0100)) { /* no message(s) */ buf[0] = '\0'; return 1; } if (r & 0x0200) { if (read(fd, &len, 4) != 4) /* read 4-byte junk */ err(errno, "unable to read %s junk response", avname); } l = 0; if (read(fd, &len, 4) != 4) /*XXX endiannes problem */ err(errno, "unable to read avp answer length"); if (len >= sizeof(buf)) /* limit response len to avoid DoSes */ len = sizeof(buf) - 1; p = buf; while(len && (l = read(fd, p, len)) > 0) { p += l; len -= l; } if (len || l < 0) err(errno, "unable to read %s answer", avname); *p = '\0'; /* now interpret avp response. */ /* we will cut junk from each line before tab character (the only interesting information, rest of line is a crap) */ t = NULL; /* last tab seen */ p = buf; /* current write pointer */ n = buf; /* current read pointer */ for(;;) { if (*n == '\t') t = ++n; else if (*n == '\n' || *n == '\0') { if (t && t != n) { memcpy(p, t, n - t); p += n - t; *p++ = '\n'; } if (!*n) break; ++n; t = NULL; } else ++n; } *p = '\0'; return 1; } #endif /* AVP */ #if DRWEB static int scan_drweb(int fd, const char *path, const char *avname) { /* DrWeb commands */ #define DRWEBD_SCAN_CMD 0x0001 /* DrWeb SCAN_CMD flags */ #define DRWEBD_RETURN_VIRUSES 0x0001 #define DRWEBD_HEURISTIC_ON 0x0008 #define DRWEBD_SCAN_FLAGS (DRWEBD_RETURN_VIRUSES/*|DRWEBD_HEURISTIC_ON*/) /* DrWeb result codes */ #define DERR_READ_ERR 0x00000001 #define DERR_WRITE_ERR 0x00000002 #define DERR_NOMEMORY 0x00000004 #define DERR_CRC_ERROR 0x00000008 #define DERR_READSOCKET 0x00000010 #define DERR_KNOWN_VIRUS 0x00000020 #define DERR_UNKNOWN_VIRUS 0x00000040 #define DERR_VIRUS_MODIFICATION 0x00000080 #define DERR_HAVE_CURED 0x00000100 #define DERR_TIMEOUT 0x00000200 #define DERR_SYMLINK 0x00000400 #define DERR_NO_REGFILE 0x00000800 #define DERR_SKIPPED 0x00001000 #define DERR_TOO_BIG 0x00002000 #define DERR_TOO_COMPRESSED 0x00004000 #define DERR_BAD_CALL 0x00008000 #define DERR_EVAL_VERSION 0x00010000 #define DERR_SPAM_MESSAGE 0x00020000 #define DERR_ARCHIVE_LEVEL 0x00040000 #define DERR_HAVE_DELETED 0x00080000 #define DERR_IS_CLEAN 0x00100000 #define DERR_VIRUS \ (DERR_KNOWN_VIRUS|DERR_UNKNOWN_VIRUS|DERR_VIRUS_MODIFICATION) int c, n; char *b, *e; b = buf; c = htonl(DRWEBD_SCAN_CMD); memcpy(b, &c, sizeof(c)); b += sizeof(c); c = htonl(DRWEBD_SCAN_FLAGS); memcpy(b, &c, sizeof(c)); b += sizeof(c); n = strlen(path); c = htonl(n); memcpy(b, &c, sizeof(c)); b += sizeof(c); memcpy(b, path, n); b += n; c = htonl(0); memcpy(b, &c, sizeof(c)); b += sizeof(c); n = b - buf; if (iwrite(fd, buf, n) < 0) err(errno, "error sending command to %s daemon", avname); if (read(fd, &c, sizeof(c)) != sizeof(c) || /* code */ read(fd, &n, sizeof(n)) != sizeof(n)) /* number of viruses */ err(errno, "error reading %s daemon response", avname); if (((c = ntohl(c)) == 0) || (c & DERR_IS_CLEAN)) return 0; /* all ok, no viruses found (n should be 0) */ if (!(c & DERR_VIRUS)) { #define DERR_SKIP_CODE (DERR_CRC_ERROR|DERR_EVAL_VERSION|DERR_SKIPPED|DERR_READ_ERR) if (!(c & ~DERR_SKIP_CODE)) { if (c & DERR_CRC_ERROR) e = "crc error (e.g. incomplete attachment)"; else if (c & DERR_SKIPPED) e = "some objects was not scanned (e.g. passwd-protected)"; else if (c & DERR_READ_ERR) e = "read error (e.g. invalid or unknown structure)"; else e = NULL; if (e) warn("unexpected %s return code %d (0x%x): %s", avname, c, c, e); return 0; } if (c & (DERR_TOO_BIG|DERR_TOO_COMPRESSED|DERR_TIMEOUT)) { strcpy(buf, "Message is too complex, possible mailbomb"); return 1; } err(0, "unexpected %s return code %d (0x%x)", avname, c, c); } n = ntohl(n); if (!n) { buf[0] = '\0'; return 1; } b = buf; e = buf + sizeof(buf) - 2; while(n) { --n; if (read(fd, &c, sizeof(c)) != sizeof(c)) break; c = ntohl(c); /* virus name length */ if (c > e - b) { c = e - b; n = 0; } if ((c = read(fd, b, c)) <= 0) { if (c < 0) break; continue; } b += c - 1; *b++ = '\n'; } *b = '\0'; return 1; } #endif /* DRWEB */ #if SOPHIE || TROPHIE static int scan_sophie_trophie(int fd, const char *path, const char *avname) { int l; char *p, *r; l = strlen(path); memcpy(buf, path, l); buf[l++] = '\n'; if (iwrite(fd, buf, l) < 0) err(errno, "unable to send a command to %s", avname); #define sp_msg "Infected by " #define sp_extra sizeof(sp_msg) r = buf + sp_extra; if ((l = read(fd, r, sizeof(buf) - 1 - sp_extra)) < 1) err(l < 0 ? errno : 0, "unable to read answer from %s", avname); if (*r == '0') return 0; r[l] = '\0'; if ((p = strchr(r, '\n')) != NULL) *p = '\0'; p = strchr(r, ':'); if (*r == '-') { if (p) { /* skip some known error messages */ ++p; if (memcmp(p, "Error: File was encrypted", 26) == 0) return 0; if (memcmp(p, "Error: File corrupted", 21) == 0) return 0; } err(0, "error in %s: return code %s", avname, r); } if (p) { ++p; while(*p == ' ' || *p == '\t') ++p; if (*p) sprintf(buf, "%s%s", sp_msg, p); else *buf = '\0'; } else *buf = '\0'; return 1; } #endif /* SOPHIE_TROPHIE */ static const struct avengine { const char *name; char *defsock; int (*scanfn)(int, const char *, const char *); } scanner[] = { #if AVP { "AVP", "/var/run/AvpCtl", scan_avp }, #endif #if DRWEB { "DrWeb", "127.0.0.1:3000", scan_drweb }, #endif #if SOPHIE { "Sophie", "/var/run/sophie", scan_sophie_trophie }, #endif #if TROPHIE { "Trophie", "/var/run/trophie", scan_sophie_trophie }, #endif { NULL, NULL, NULL } }; /* like strtok */ static char *nextword(char *str) { static char *p; /* = NULL */ static const char *delim = " \t\r\n"; if (str) p = str; while(*p && strchr(delim, *p)) ++p; if (!*p) return NULL; str = p++; while(*p && !strchr(delim, *p)) ++p; if (*p) *p++ = '\0'; return str; } int main(int argc, char **argv) { int c; char *p; int avfd; /* antivirus daemon socket */ int msgfd; /* message fd */ /* options */ const struct avengine *engine = NULL; char *avsocket = NULL; /* av control socket */ char *tmpdir = NULL; /* file-based access directory */ char *from = NULL; int nosend = 0; /* do not send good mails using sendmail */ int mailclient = 0; /* emulate mail client for injecting mail */ char *waitfile = NULL; unsigned avtimeout = 0; /* timeout for antivirus daemon operation */ char *okhdr = NULL; /* header to add for good mails */ int okcode = 0; char *ipath = argv[0]; char *iscript = NULL; if ((progname = strrchr(argv[0], '/')) != NULL) argv[0] = ++progname; else progname = argv[0]; if (argc == 1 || (argc == 2 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")))) { printf("\ %s: mail virus-checker version " VERSION ".\n\ Usage: %s options -- recipient..., where options are:\n\ -f sender - sender's envelope address (required)\n\ -s type[:socket] - antivirus daemon type and it's control\n\ socket (either /path/to/file or host:port). Supported\n\ antivirus engines and default control sockets are:\n", progname, progname); for (c = 0; scanner[c].name; ++c) printf("\t%s\t%s\n", scanner[c].name, scanner[c].defsock); printf("\ -d dir - place files to this temporary directory when\n\ inspecting (use /some/where/./dir if antivirus is chrooted\n\ to /some/where) (required)\n\ -t timeout - timeout, in secounds, to wait for antivirus\n\ answer (default is 0, i.e. no timeout)\n\ -n - do not send (reinject) good mail back into the mail system\n\ -g code - return this code for good, uninfected emails\n\ -S sendmail_path - /path/to/sendmail-compatible executable\n\ (possible with args -- either using multi-word value or repeating\n\ this option to specify additional arguments) or host:port to speak\n\ subset of SMTP (default is port 25 on localhost)\n\ -h hdr - prepend \"X-AV-Checked: