/* $Cambridge: exim/src/src/malware.c,v 1.6 2005/01/13 10:09:36 ph10 Exp $ */ /************************************************* * Exim - an Internet mail transport agent * *************************************************/ /* Copyright (c) Tom Kistner 2003-???? */ /* License: GPL */ /* Code for calling virus (malware) scanners. Called from acl.c. */ #include "exim.h" #ifdef WITH_CONTENT_SCAN /* declaration of private routines */ int mksd_scan_packed(int sock); /* SHUT_WR seems to be undefined on Unixware? */ #ifndef SHUT_WR #define SHUT_WR 1 #endif #define DRWEBD_SCAN_CMD (1) /* scan file, buffer or diskfile */ #define DRWEBD_RETURN_VIRUSES (1<<0) /* ask daemon return to us viruses names from report */ #define DRWEBD_IS_MAIL (1<<19) /* say to daemon that format is "archive MAIL" */ #define DERR_READ_ERR (1<<0) /* read error */ #define DERR_NOMEMORY (1<<2) /* no memory */ #define DERR_TIMEOUT (1<<9) /* scan timeout has run out */ #define DERR_BAD_CALL (1<<15) /* wrong command */ /* Routine to check whether a system is big- or litte-endian. Ripped from http://www.faqs.org/faqs/graphics/fileformats-faq/part4/section-7.html Needed for proper kavdaemon implementation. Sigh. */ #define BIG_MY_ENDIAN 0 #define LITTLE_MY_ENDIAN 1 int test_byte_order(void); int test_byte_order() { short int word = 0x0001; char *byte = (char *) &word; return(byte[0] ? LITTLE_MY_ENDIAN : BIG_MY_ENDIAN); } uschar malware_name_buffer[256]; int malware_ok = 0; int malware(uschar **listptr) { int sep = 0; uschar *list = *listptr; uschar *av_scanner_work = av_scanner; uschar *scanner_name; uschar scanner_name_buffer[16]; uschar *malware_regex; uschar malware_regex_buffer[64]; uschar malware_regex_default[] = ".+"; unsigned long mbox_size; FILE *mbox_file; int roffset; const pcre *re; const uschar *rerror; /* make sure the eml mbox file is spooled up */ mbox_file = spool_mbox(&mbox_size); if (mbox_file == NULL) { /* error while spooling */ log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: error while creating mbox spool file"); return DEFER; }; /* none of our current scanners need the mbox file as a stream, so we can close it right away */ fclose(mbox_file); /* extract the malware regex to match against from the option list */ if ((malware_regex = string_nextinlist(&list, &sep, malware_regex_buffer, sizeof(malware_regex_buffer))) != NULL) { /* parse 1st option */ if ( (strcmpic(malware_regex,US"false") == 0) || (Ustrcmp(malware_regex,"0") == 0) ) { /* explicitly no matching */ return FAIL; }; /* special cases (match anything except empty) */ if ( (strcmpic(malware_regex,US"true") == 0) || (Ustrcmp(malware_regex,"*") == 0) || (Ustrcmp(malware_regex,"1") == 0) ) { malware_regex = malware_regex_default; }; } else { /* empty means "don't match anything" */ return FAIL; }; /* Reset sep that is set by previous string_nextinlist() call */ sep = 0; /* compile the regex, see if it works */ re = pcre_compile(CS malware_regex, PCRE_COPT, (const char **)&rerror, &roffset, NULL); if (re == NULL) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: regular expression error in '%s': %s at offset %d", malware_regex, rerror, roffset); return DEFER; }; /* if av_scanner starts with a dollar, expand it first */ if (*av_scanner == '$') { av_scanner_work = expand_string(av_scanner); if (av_scanner_work == NULL) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: av_scanner starts with $, but expansion failed: %s", expand_string_message); return DEFER; } else { debug_printf("Expanded av_scanner global: %s\n", av_scanner_work); /* disable result caching in this case */ malware_name = NULL; malware_ok = 0; }; } /* Do not scan twice. */ if (malware_ok == 0) { /* find the scanner type from the av_scanner option */ if ((scanner_name = string_nextinlist(&av_scanner_work, &sep, scanner_name_buffer, sizeof(scanner_name_buffer))) == NULL) { /* no scanner given */ log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: av_scanner configuration variable is empty"); return DEFER; }; /* "drweb" scanner type ----------------------------------------------- */ /* v0.1 - added support for tcp sockets */ /* v0.0 - initial release -- support for unix sockets */ if (strcmpic(scanner_name,US"drweb") == 0) { uschar *drweb_options; uschar drweb_options_buffer[1024]; uschar drweb_options_default[] = "/usr/local/drweb/run/drwebd.sock"; struct sockaddr_un server; int sock, result, ovector[30]; unsigned int port, fsize; uschar tmpbuf[1024], *drweb_fbuf; uschar scanrequest[1024]; uschar drweb_match_string[128]; int drweb_rc, drweb_cmd, drweb_flags = 0x0000, drweb_fd, drweb_vnum, drweb_slen, drweb_fin = 0x0000; unsigned long bread; uschar hostname[256]; struct hostent *he; struct in_addr in; pcre *drweb_re; if ((drweb_options = string_nextinlist(&av_scanner_work, &sep, drweb_options_buffer, sizeof(drweb_options_buffer))) == NULL) { /* no options supplied, use default options */ drweb_options = drweb_options_default; }; if (*drweb_options != '/') { /* extract host and port part */ if( sscanf(CS drweb_options, "%s %u", hostname, &port) != 2 ) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: invalid socket '%s'", drweb_options); return DEFER; } /* Lookup the host */ if((he = gethostbyname(CS hostname)) == 0) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: failed to lookup host '%s'", hostname); return DEFER; } in = *(struct in_addr *) he->h_addr_list[0]; /* Open the drwebd TCP socket */ if ( (sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: unable to acquire socket (%s)", strerror(errno)); return DEFER; } if (ip_connect(sock, AF_INET, (uschar*)inet_ntoa(in), port, 5) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: connection to %s, port %u failed (%s)", inet_ntoa(in), port, strerror(errno)); return DEFER; } /* prepare variables */ drweb_cmd = htonl(DRWEBD_SCAN_CMD); drweb_flags = htonl(DRWEBD_RETURN_VIRUSES | DRWEBD_IS_MAIL); snprintf(CS scanrequest, 1024,CS"%s/scan/%s/%s.eml", spool_directory, message_id, message_id); /* calc file size */ drweb_fd = open(CS scanrequest, O_RDONLY); if (drweb_fd == -1) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: can't open spool file %s: %s", scanrequest, strerror(errno)); return DEFER; } fsize = lseek(drweb_fd, 0, SEEK_END); if (fsize == -1) { close(sock); close(drweb_fd); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: can't seek spool file %s: %s", scanrequest, strerror(errno)); return DEFER; } drweb_slen = htonl(fsize); lseek(drweb_fd, 0, SEEK_SET); /* send scan request */ if ((send(sock, &drweb_cmd, sizeof(drweb_cmd), 0) < 0) || (send(sock, &drweb_flags, sizeof(drweb_flags), 0) < 0) || (send(sock, &drweb_fin, sizeof(drweb_fin), 0) < 0) || (send(sock, &drweb_slen, sizeof(drweb_slen), 0) < 0)) { close(sock); close(drweb_fd); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: unable to send commands to socket (%s)", drweb_options); return DEFER; } drweb_fbuf = (uschar *) malloc (fsize); if (!drweb_fbuf) { close(sock); close(drweb_fd); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: unable to allocate memory %u for file (%s)", fsize, scanrequest); return DEFER; } result = read (drweb_fd, drweb_fbuf, fsize); if (result == -1) { close(sock); close(drweb_fd); free(drweb_fbuf); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: can't read spool file %s: %s", scanrequest, strerror(errno)); return DEFER; } close(drweb_fd); /* send file body to socket */ if (send(sock, drweb_fbuf, fsize, 0) < 0) { close(sock); free(drweb_fbuf); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: unable to send file body to socket (%s)", drweb_options); return DEFER; } close(drweb_fd); } else { /* open the drwebd UNIX socket */ sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: can't open UNIX socket"); return DEFER; } server.sun_family = AF_UNIX; Ustrcpy(server.sun_path, drweb_options); if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: unable to connect to socket (%s). errno=%d", drweb_options, errno); return DEFER; } /* prepare variables */ drweb_cmd = htonl(DRWEBD_SCAN_CMD); drweb_flags = htonl(DRWEBD_RETURN_VIRUSES | DRWEBD_IS_MAIL); snprintf(CS scanrequest, 1024,CS"%s/scan/%s/%s.eml", spool_directory, message_id, message_id); drweb_slen = htonl(Ustrlen(scanrequest)); /* send scan request */ if ((send(sock, &drweb_cmd, sizeof(drweb_cmd), 0) < 0) || (send(sock, &drweb_flags, sizeof(drweb_flags), 0) < 0) || (send(sock, &drweb_slen, sizeof(drweb_slen), 0) < 0) || (send(sock, scanrequest, Ustrlen(scanrequest), 0) < 0) || (send(sock, &drweb_fin, sizeof(drweb_fin), 0) < 0)) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: unable to send commands to socket (%s)", drweb_options); return DEFER; } } /* wait for result */ if ((bread = recv(sock, &drweb_rc, sizeof(drweb_rc), 0) != sizeof(drweb_rc))) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: unable to read return code"); return DEFER; } drweb_rc = ntohl(drweb_rc); if ((bread = recv(sock, &drweb_vnum, sizeof(drweb_vnum), 0) != sizeof(drweb_vnum))) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: unable to read the number of viruses"); return DEFER; } drweb_vnum = ntohl(drweb_vnum); /* "virus(es) found" if virus number is > 0 */ if (drweb_vnum) { int i; uschar pre_malware_nb[256]; malware_name = malware_name_buffer; /* setup default virus name */ Ustrcpy(malware_name_buffer,"unknown"); /* read and concatenate virus names into one string */ for (i=0;i= 2) { pcre_copy_substring(CS tmpbuf, ovector, result, 1, CS pre_malware_nb, 255); } /* the first name we just copy to malware_name */ if (i==0) Ustrcpy(CS malware_name_buffer, CS pre_malware_nb); else { /* concatenate each new virus name to previous */ int slen = Ustrlen(malware_name_buffer); if (slen < (slen+Ustrlen(pre_malware_nb))) { Ustrcat(malware_name_buffer, "/"); Ustrcat(malware_name_buffer, pre_malware_nb); } } } } else { char *drweb_s = NULL; if (drweb_rc & DERR_READ_ERR) drweb_s = "read error"; if (drweb_rc & DERR_NOMEMORY) drweb_s = "no memory"; if (drweb_rc & DERR_TIMEOUT) drweb_s = "timeout"; if (drweb_rc & DERR_BAD_CALL) drweb_s = "wrong command"; /* retcodes DERR_SYMLINK, DERR_NO_REGFILE, DERR_SKIPPED. * DERR_TOO_BIG, DERR_TOO_COMPRESSED, DERR_SPAM, * DERR_CRC_ERROR, DERR_READSOCKET, DERR_WRITE_ERR * and others are ignored */ if (drweb_s) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: drweb: drweb daemon retcode 0x%x (%s)", drweb_rc, drweb_s); close(sock); return DEFER; } /* no virus found */ malware_name = NULL; }; close(sock); } /* ----------------------------------------------------------------------- */ else if (strcmpic(scanner_name,US"aveserver") == 0) { uschar *kav_options; uschar kav_options_buffer[1024]; uschar kav_options_default[] = "/var/run/aveserver"; uschar buf[32768]; struct sockaddr_un server; int sock; if ((kav_options = string_nextinlist(&av_scanner_work, &sep, kav_options_buffer, sizeof(kav_options_buffer))) == NULL) { /* no options supplied, use default options */ kav_options = kav_options_default; }; /* open the aveserver socket */ sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: can't open UNIX socket."); return DEFER; } server.sun_family = AF_UNIX; Ustrcpy(server.sun_path, kav_options); if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to connect to aveserver UNIX socket (%s). errno=%d", kav_options, errno); return DEFER; } /* read aveserver's greeting and see if it is ready (2xx greeting) */ recv_line(sock, buf, 32768); if (buf[0] != '2') { /* aveserver is having problems */ close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: aveserver is unavailable (Responded: %s).", ((buf[0] != 0) ? buf : (uschar *)"nothing") ); return DEFER; }; /* prepare our command */ snprintf(CS buf, 32768, "SCAN bPQRSTUW %s/scan/%s/%s.eml\r\n", spool_directory, message_id, message_id); /* and send it */ if (send(sock, buf, Ustrlen(buf), 0) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to write to aveserver UNIX socket (%s)", kav_options); return DEFER; } malware_name = NULL; /* read response lines, find malware name and final response */ while (recv_line(sock, buf, 32768) > 0) { debug_printf("aveserver: %s\n", buf); if (buf[0] == '2') break; if (Ustrncmp(buf,"322",3) == 0) { uschar *p = Ustrchr(&buf[4],' '); *p = '\0'; Ustrcpy(malware_name_buffer,&buf[4]); malware_name = malware_name_buffer; }; } close(sock); } /* "fsecure" scanner type ------------------------------------------------- */ else if (strcmpic(scanner_name,US"fsecure") == 0) { uschar *fsecure_options; uschar fsecure_options_buffer[1024]; uschar fsecure_options_default[] = "/var/run/.fsav"; struct sockaddr_un server; int sock, i, j, bread = 0; uschar file_name[1024]; uschar av_buffer[1024]; pcre *fs_inf; static uschar *cmdoptions[] = { US"CONFIGURE\tARCHIVE\t1\n", US"CONFIGURE\tTIMEOUT\t0\n", US"CONFIGURE\tMAXARCH\t5\n", US"CONFIGURE\tMIME\t1\n" }; malware_name = NULL; if ((fsecure_options = string_nextinlist(&av_scanner_work, &sep, fsecure_options_buffer, sizeof(fsecure_options_buffer))) == NULL) { /* no options supplied, use default options */ fsecure_options = fsecure_options_default; }; /* open the fsecure socket */ sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to open fsecure socket %s (%s)", fsecure_options, strerror(errno)); return DEFER; } server.sun_family = AF_UNIX; Ustrcpy(server.sun_path, fsecure_options); if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to connect to fsecure socket %s (%s)", fsecure_options, strerror(errno)); return DEFER; } /* pass options */ memset(av_buffer, 0, sizeof(av_buffer)); for (i=0; i != 4; i++) { /* debug_printf("send option \"%s\"",cmdoptions[i]); */ if (write(sock, cmdoptions[i], Ustrlen(cmdoptions[i])) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to write fsecure option %d to %s (%s)", i, fsecure_options, strerror(errno)); return DEFER; }; bread = read(sock, av_buffer, sizeof(av_buffer)); if (bread >0) av_buffer[bread]='\0'; if (bread < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to read fsecure answer %d (%s)", i, strerror(errno)); return DEFER; }; for (j=0;j= 2) { /* Got it */ pcre_copy_substring(CS av_buffer, ovector, i, 1, CS malware_name_buffer, 255); malware_name = malware_name_buffer; }; }; } while (Ustrstr(av_buffer, "OK\tScan ok.") == NULL); close(sock); } /* ----------------------------------------------------------------------- */ /* "kavdaemon" scanner type ------------------------------------------------ */ else if (strcmpic(scanner_name,US"kavdaemon") == 0) { uschar *kav_options; uschar kav_options_buffer[1024]; uschar kav_options_default[] = "/var/run/AvpCtl"; struct sockaddr_un server; int sock; time_t t; uschar tmpbuf[1024]; uschar scanrequest[1024]; uschar kav_match_string[128]; int kav_rc; unsigned long kav_reportlen, bread; pcre *kav_re; if ((kav_options = string_nextinlist(&av_scanner_work, &sep, kav_options_buffer, sizeof(kav_options_buffer))) == NULL) { /* no options supplied, use default options */ kav_options = kav_options_default; }; /* open the kavdaemon socket */ sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: can't open UNIX socket."); return DEFER; } server.sun_family = AF_UNIX; Ustrcpy(server.sun_path, kav_options); if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to connect to kavdaemon UNIX socket (%s). errno=%d", kav_options, errno); return DEFER; } /* get current date and time, build scan request */ time(&t); strftime(CS tmpbuf, sizeof(tmpbuf), "<0>%d %b %H:%M:%S:%%s/scan/%%s", localtime(&t)); snprintf(CS scanrequest, 1024,CS tmpbuf, spool_directory, message_id); /* send scan request */ if (send(sock, scanrequest, Ustrlen(scanrequest)+1, 0) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to write to kavdaemon UNIX socket (%s)", kav_options); return DEFER; } /* wait for result */ if ((bread = recv(sock, tmpbuf, 2, 0) != 2)) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to read 2 bytes from kavdaemon socket."); return DEFER; } /* get errorcode from one nibble */ if (test_byte_order() == LITTLE_MY_ENDIAN) { kav_rc = tmpbuf[0] & 0x0F; } else { kav_rc = tmpbuf[1] & 0x0F; }; /* improper kavdaemon configuration */ if ( (kav_rc == 5) || (kav_rc == 6) ) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: please reconfigure kavdaemon to NOT disinfect or remove infected files."); return DEFER; }; if (kav_rc == 1) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: kavdaemon reported 'scanning not completed' (code 1)."); return DEFER; }; if (kav_rc == 7) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: kavdaemon reported 'kavdaemon damaged' (code 7)."); return DEFER; }; /* code 8 is not handled, since it is ambigous. It appears mostly on bounces where part of a file has been cut off */ /* "virus found" return codes (2-4) */ if ((kav_rc > 1) && (kav_rc < 5)) { int report_flag = 0; /* setup default virus name */ Ustrcpy(malware_name_buffer,"unknown"); malware_name = malware_name_buffer; if (test_byte_order() == LITTLE_MY_ENDIAN) { report_flag = tmpbuf[1]; } else { report_flag = tmpbuf[0]; }; /* read the report, if available */ if( report_flag == 1 ) { /* read report size */ if ((bread = recv(sock, &kav_reportlen, 4, 0)) != 4) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: cannot read report size from kavdaemon"); return DEFER; }; /* it's possible that avp returns av_buffer[1] == 1 but the reportsize is 0 (!?) */ if (kav_reportlen > 0) { /* set up match regex, depends on retcode */ if( kav_rc == 3 ) Ustrcpy(kav_match_string, "suspicion:\\s*(.+?)\\s*$"); else Ustrcpy(kav_match_string, "infected:\\s*(.+?)\\s*$"); kav_re = pcre_compile( CS kav_match_string, PCRE_COPT, (const char **)&rerror, &roffset, NULL ); /* read report, linewise */ while (kav_reportlen > 0) { int result = 0; int ovector[30]; bread = 0; while ( recv(sock, &tmpbuf[bread], 1, 0) == 1 ) { kav_reportlen--; if ( (tmpbuf[bread] == '\n') || (bread > 1021) ) break; bread++; }; bread++; tmpbuf[bread] = '\0'; /* try matcher on the line, grab substring */ result = pcre_exec(kav_re, NULL, CS tmpbuf, Ustrlen(tmpbuf), 0, 0, ovector, 30); if (result >= 2) { pcre_copy_substring(CS tmpbuf, ovector, result, 1, CS malware_name_buffer, 255); break; }; }; }; }; } else { /* no virus found */ malware_name = NULL; }; close(sock); } /* ----------------------------------------------------------------------- */ /* "cmdline" scanner type ------------------------------------------------ */ else if (strcmpic(scanner_name,US"cmdline") == 0) { uschar *cmdline_scanner; uschar cmdline_scanner_buffer[1024]; uschar *cmdline_trigger; uschar cmdline_trigger_buffer[1024]; const pcre *cmdline_trigger_re; uschar *cmdline_regex; uschar cmdline_regex_buffer[1024]; const pcre *cmdline_regex_re; uschar file_name[1024]; uschar commandline[1024]; void (*eximsigchld)(int); void (*eximsigpipe)(int); FILE *scanner_out = NULL; FILE *scanner_record = NULL; uschar linebuffer[32767]; int trigger = 0; int result; int ovector[30]; /* find scanner command line */ if ((cmdline_scanner = string_nextinlist(&av_scanner_work, &sep, cmdline_scanner_buffer, sizeof(cmdline_scanner_buffer))) == NULL) { /* no command line supplied */ log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: missing commandline specification for cmdline scanner type."); return DEFER; }; /* find scanner output trigger */ if ((cmdline_trigger = string_nextinlist(&av_scanner_work, &sep, cmdline_trigger_buffer, sizeof(cmdline_trigger_buffer))) == NULL) { /* no trigger regex supplied */ log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: missing trigger specification for cmdline scanner type."); return DEFER; }; /* precompile trigger regex */ cmdline_trigger_re = pcre_compile(CS cmdline_trigger, PCRE_COPT, (const char **)&rerror, &roffset, NULL); if (cmdline_trigger_re == NULL) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: regular expression error in '%s': %s at offset %d", cmdline_trigger_re, rerror, roffset); return DEFER; }; /* find scanner name regex */ if ((cmdline_regex = string_nextinlist(&av_scanner_work, &sep, cmdline_regex_buffer, sizeof(cmdline_regex_buffer))) == NULL) { /* no name regex supplied */ log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: missing virus name regex specification for cmdline scanner type."); return DEFER; }; /* precompile name regex */ cmdline_regex_re = pcre_compile(CS cmdline_regex, PCRE_COPT, (const char **)&rerror, &roffset, NULL); if (cmdline_regex_re == NULL) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: regular expression error in '%s': %s at offset %d", cmdline_regex_re, rerror, roffset); return DEFER; }; /* prepare scanner call */ snprintf(CS file_name,1024,"%s/scan/%s", spool_directory, message_id); snprintf(CS commandline,1024, CS cmdline_scanner,file_name); /* redirect STDERR too */ Ustrcat(commandline," 2>&1"); /* store exims signal handlers */ eximsigchld = signal(SIGCHLD,SIG_DFL); eximsigpipe = signal(SIGPIPE,SIG_DFL); scanner_out = popen(CS commandline,"r"); if (scanner_out == NULL) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: calling cmdline scanner (%s) failed: %s.", commandline, strerror(errno)); signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe); return DEFER; }; snprintf(CS file_name,1024,"%s/scan/%s/%s_scanner_output", spool_directory, message_id, message_id); scanner_record = fopen(CS file_name,"w"); if (scanner_record == NULL) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: opening scanner output file (%s) failed: %s.", file_name, strerror(errno)); pclose(scanner_out); signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe); return DEFER; }; /* look for trigger while recording output */ while(fgets(CS linebuffer,32767,scanner_out) != NULL) { if ( Ustrlen(linebuffer) > fwrite(linebuffer, 1, Ustrlen(linebuffer), scanner_record) ) { /* short write */ log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: short write on scanner output file (%s).", file_name); pclose(scanner_out); signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe); return DEFER; }; /* try trigger match */ if (!trigger && regex_match_and_setup(cmdline_trigger_re, linebuffer, 0, -1)) trigger = 1; }; fclose(scanner_record); pclose(scanner_out); signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe); if (trigger) { /* setup default virus name */ Ustrcpy(malware_name_buffer,"unknown"); malware_name = malware_name_buffer; /* re-open the scanner output file, look for name match */ scanner_record = fopen(CS file_name,"r"); while(fgets(CS linebuffer,32767,scanner_record) != NULL) { /* try match */ result = pcre_exec(cmdline_regex_re, NULL, CS linebuffer, Ustrlen(linebuffer), 0, 0, ovector, 30); if (result >= 2) { pcre_copy_substring(CS linebuffer, ovector, result, 1, CS malware_name_buffer, 255); }; }; fclose(scanner_record); } else { /* no virus found */ malware_name = NULL; }; } /* ----------------------------------------------------------------------- */ /* "sophie" scanner type ------------------------------------------------- */ else if (strcmpic(scanner_name,US"sophie") == 0) { uschar *sophie_options; uschar sophie_options_buffer[1024]; uschar sophie_options_default[] = "/var/run/sophie"; int bread = 0; struct sockaddr_un server; int sock; uschar file_name[1024]; uschar av_buffer[1024]; if ((sophie_options = string_nextinlist(&av_scanner_work, &sep, sophie_options_buffer, sizeof(sophie_options_buffer))) == NULL) { /* no options supplied, use default options */ sophie_options = sophie_options_default; }; /* open the sophie socket */ sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: can't open UNIX socket."); return DEFER; } server.sun_family = AF_UNIX; Ustrcpy(server.sun_path, sophie_options); if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to connect to sophie UNIX socket (%s). errno=%d", sophie_options, errno); return DEFER; } /* pass the scan directory to sophie */ snprintf(CS file_name,1024,"%s/scan/%s", spool_directory, message_id); if (write(sock, file_name, Ustrlen(file_name)) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to write to sophie UNIX socket (%s)", sophie_options); return DEFER; }; write(sock, "\n", 1); /* wait for result */ memset(av_buffer, 0, sizeof(av_buffer)); if ((!(bread = read(sock, av_buffer, sizeof(av_buffer))) > 0)) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to read from sophie UNIX socket (%s)", sophie_options); return DEFER; }; close(sock); /* infected ? */ if (av_buffer[0] == '1') { if (Ustrchr(av_buffer, '\n')) *Ustrchr(av_buffer, '\n') = '\0'; Ustrcpy(malware_name_buffer,&av_buffer[2]); malware_name = malware_name_buffer; } else if (!strncmp(CS av_buffer, "-1", 2)) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: malware acl condition: sophie reported error"); return DEFER; } else { /* all ok, no virus */ malware_name = NULL; }; } /* ----------------------------------------------------------------------- */ /* "clamd" scanner type ------------------------------------------------- */ /* This code was contributed by David Saez */ else if (strcmpic(scanner_name,US"clamd") == 0) { uschar *clamd_options; uschar clamd_options_buffer[1024]; uschar clamd_options_default[] = "/tmp/clamd"; uschar *p,*vname; struct sockaddr_un server; int sock,bread=0; unsigned int port; uschar file_name[1024]; uschar av_buffer[1024]; uschar hostname[256]; struct hostent *he; struct in_addr in; uschar *clamd_options2; uschar clamd_options2_buffer[1024]; uschar clamd_options2_default[] = ""; uschar av_buffer2[1024]; uschar *clamav_fbuf; uschar scanrequest[1024]; int sockData, clam_fd, result; unsigned int fsize; if ((clamd_options = string_nextinlist(&av_scanner_work, &sep, clamd_options_buffer, sizeof(clamd_options_buffer))) == NULL) { /* no options supplied, use default options */ clamd_options = clamd_options_default; } if ((clamd_options2 = string_nextinlist(&av_scanner_work, &sep, clamd_options2_buffer, sizeof(clamd_options2_buffer))) == NULL) { clamd_options2 = clamd_options2_default; } /* socket does not start with '/' -> network socket */ if (*clamd_options != '/') { /* extract host and port part */ if( sscanf(CS clamd_options, "%s %u", hostname, &port) != 2 ) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: invalid socket '%s'", clamd_options); return DEFER; }; /* Lookup the host */ if((he = gethostbyname(CS hostname)) == 0) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: failed to lookup host '%s'", hostname); return DEFER; } in = *(struct in_addr *) he->h_addr_list[0]; /* Open the ClamAV Socket */ if ( (sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: unable to acquire socket (%s)", strerror(errno)); return DEFER; } if (ip_connect(sock, AF_INET, (uschar*)inet_ntoa(in), port, 5) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: connection to %s, port %u failed (%s)", inet_ntoa(in), port, strerror(errno)); return DEFER; } if (strcmpic(clamd_options2,US"local") == 0) { /* Pass the string to ClamAV (7 = "SCAN \n" + \0) */ snprintf(CS file_name,1024,"SCAN %s/scan/%s\n", spool_directory, message_id); if (send(sock, file_name, Ustrlen(file_name), 0) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC,"malware acl condition: clamd: unable to write to socket (%s)", strerror(errno)); return DEFER; } } else { /* Pass the string to ClamAV (7 = "STREAM\n") */ if (send(sock, "STREAM\n", 7, 0) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC,"malware acl condition: clamd: unable to write to socket (%s)", strerror(errno)); return DEFER; } memset(av_buffer2, 0, sizeof(av_buffer2)); bread = read(sock, av_buffer2, sizeof(av_buffer2)); if (bread < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: unable to read PORT from socket (%s)", strerror(errno)); return DEFER; } if (bread == sizeof(av_buffer)) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: buffer too small"); return DEFER; } if (!(*av_buffer2)) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: ClamAV returned null"); return DEFER; } av_buffer2[bread] = '\0'; if( sscanf(CS av_buffer2, "PORT %u\n", &port) != 1 ) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: Expected port information from clamd, got '%s'", av_buffer2); return DEFER; }; if ( (sockData = ip_socket(SOCK_STREAM, AF_INET)) < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: unable to acquire socket (%s)", strerror(errno)); return DEFER; } if (ip_connect(sockData, AF_INET, (uschar*)inet_ntoa(in), port, 5) < 0) { close(sockData); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: connection to %s, port %u failed (%s)", inet_ntoa(in), port, strerror(errno)); return DEFER; } snprintf(CS scanrequest, 1024,CS"%s/scan/%s/%s.eml", spool_directory, message_id, message_id); /* calc file size */ clam_fd = open(CS scanrequest, O_RDONLY); if (clam_fd == -1) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: can't open spool file %s: %s", scanrequest, strerror(errno)); return DEFER; } fsize = lseek(clam_fd, 0, SEEK_END); if (fsize == -1) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: can't seek spool file %s: %s", scanrequest, strerror(errno)); return DEFER; } lseek(clam_fd, 0, SEEK_SET); clamav_fbuf = (uschar *) malloc (fsize); if (!clamav_fbuf) { close(sockData); close(clam_fd); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: unable to allocate memory %u for file (%s)", fsize, scanrequest); return DEFER; } result = read (clam_fd, clamav_fbuf, fsize); if (result == -1) { close(sockData); close(clam_fd); free(clamav_fbuf); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: can't read spool file %s: %s", scanrequest, strerror(errno)); return DEFER; } close(clam_fd); /* send file body to socket */ if (send(sockData, clamav_fbuf, fsize, 0) < 0) { close(sockData); free(clamav_fbuf); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: unable to send file body to socket (%s:%u)", hostname, port); return DEFER; } free(clamav_fbuf); close(sockData); } } else { /* open the local socket */ if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: unable to acquire socket (%s)", strerror(errno)); return DEFER; } server.sun_family = AF_UNIX; Ustrcpy(server.sun_path, clamd_options); if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: unable to connect to UNIX socket %s (%s)", clamd_options, strerror(errno) ); return DEFER; } } /* Pass the string to ClamAV (7 = "SCAN \n" + \0) */ snprintf(CS file_name,1024,"SCAN %s/scan/%s\n", spool_directory, message_id); if (send(sock, file_name, Ustrlen(file_name), 0) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC,"malware acl condition: clamd: unable to write to socket (%s)", strerror(errno)); return DEFER; } /* We're done sending, close socket for writing. One user reported that clamd 0.70 does not like this any more ... */ /* shutdown(sock, SHUT_WR); */ /* Read the result */ memset(av_buffer, 0, sizeof(av_buffer)); bread = read(sock, av_buffer, sizeof(av_buffer)); close(sock); if (!(bread > 0)) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: unable to read from socket (%s)", strerror(errno)); return DEFER; } if (bread == sizeof(av_buffer)) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: buffer too small"); return DEFER; } /* Check the result. ClamAV Returns infected: -> ": FOUND" not-infected: -> ": OK" error: -> ": ERROR */ if (!(*av_buffer)) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: ClamAV returned null"); return DEFER; } /* colon in returned output? */ if((p = Ustrrchr(av_buffer,':')) == NULL) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: ClamAV returned malformed result: %s", av_buffer); return DEFER; } /* strip filename strip CR at the end */ ++p; while (*p == ' ') ++p; vname = p; p = vname + Ustrlen(vname) - 1; if( *p == '\n' ) *p = '\0'; if ((p = Ustrstr(vname, "FOUND"))!=NULL) { *p=0; for (--p;p>vname && *p<=32;p--) *p=0; for (;*vname==32;vname++); Ustrcpy(malware_name_buffer,vname); malware_name = malware_name_buffer; } else { if (Ustrstr(vname, "ERROR")!=NULL) { /* ClamAV reports ERROR Find line start */ for (;*vname!='\n' && vname>av_buffer; vname--); if (*vname=='\n') vname++; log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: clamd: ClamAV returned %s",vname); return DEFER; } else { /* Everything should be OK */ malware_name = NULL; } } } /* ----------------------------------------------------------------------- */ /* "mksd" scanner type --------------------------------------------------- */ else if (strcmpic(scanner_name,US"mksd") == 0) { uschar *mksd_options; char *mksd_options_end; uschar mksd_options_buffer[32]; int mksd_maxproc = 1; /* default, if no option supplied */ struct sockaddr_un server; int sock; int retval; if ((mksd_options = string_nextinlist(&av_scanner_work, &sep, mksd_options_buffer, sizeof(mksd_options_buffer))) != NULL) { mksd_maxproc = (int) strtol(CS mksd_options, &mksd_options_end, 10); if ((*mksd_options == '\0') || (*mksd_options_end != '\0') || (mksd_maxproc < 1) || (mksd_maxproc > 32)) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: mksd: invalid option '%s'", mksd_options); return DEFER; } } /* open the mksd socket */ sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock < 0) { log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: can't open UNIX socket."); return DEFER; } server.sun_family = AF_UNIX; Ustrcpy(server.sun_path, "/var/run/mksd/socket"); if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) { close(sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to connect to mksd UNIX socket (/var/run/mksd/socket). errno=%d", errno); return DEFER; } malware_name = NULL; retval = mksd_scan_packed(sock); if (retval != OK) return retval; } /* ----------------------------------------------------------------------- */ /* "unknown" scanner type ------------------------------------------------- */ else { log_write(0, LOG_MAIN|LOG_PANIC, "malware condition: unknown scanner type '%s'", scanner_name); return DEFER; }; /* ----------------------------------------------------------------------- */ /* set "been here, done that" marker */ malware_ok = 1; }; /* match virus name against pattern (caseless ------->----------v) */ if ( (malware_name != NULL) && (regex_match_and_setup(re, malware_name, 0, -1)) ) { return OK; } else { return FAIL; }; } /* simple wrapper for reading lines from sockets */ int recv_line(int sock, uschar *buffer, int size) { uschar *p = buffer; memset(buffer,0,size); /* read until \n */ while(recv(sock,p,1,0) > -1) { if ((p-buffer) > (size-2)) break; if (*p == '\n') break; if (*p != '\r') p++; }; *p = '\0'; return (p-buffer); } /* ============= private routines for the "mksd" scanner type ============== */ #include int mksd_writev (int sock, struct iovec *iov, int iovcnt) { int i; for (;;) { do i = writev (sock, iov, iovcnt); while ((i < 0) && (errno == EINTR)); if (i <= 0) { close (sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to write to mksd UNIX socket (/var/run/mksd/socket)"); return -1; } for (;;) if (i >= iov->iov_len) { if (--iovcnt == 0) return 0; i -= iov->iov_len; iov++; } else { iov->iov_len -= i; iov->iov_base = CS iov->iov_base + i; break; } } } int mksd_read_lines (int sock, uschar *av_buffer, int av_buffer_size) { int offset = 0; int i; do { if ((i = recv (sock, av_buffer+offset, av_buffer_size-offset, 0)) <= 0) { close (sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: unable to read from mksd UNIX socket (/var/run/mksd/socket)"); return -1; } offset += i; /* offset == av_buffer_size -> buffer full */ if (offset == av_buffer_size) { close (sock); log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: malformed reply received from mksd"); return -1; } } while (av_buffer[offset-1] != '\n'); av_buffer[offset] = '\0'; return offset; } int mksd_parse_line (char *line) { char *p; switch (*line) { case 'O': /* OK */ return OK; case 'E': case 'A': /* ERR */ if ((p = strchr (line, '\n')) != NULL) (*p) = '\0'; log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: mksd scanner failed: %s", line); return DEFER; default: /* VIR */ if ((p = strchr (line, '\n')) != NULL) { (*p) = '\0'; if (((p-line) > 5) && ((p-line) < sizeof (malware_name_buffer)) && (line[3] == ' ')) if (((p = strchr (line+4, ' ')) != NULL) && ((p-line) > 4)) { (*p) = '\0'; Ustrcpy (malware_name_buffer, line+4); malware_name = malware_name_buffer; return OK; } } log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: malformed reply received from mksd: %s", line); return DEFER; } } int mksd_scan_packed (int sock) { struct iovec iov[7]; char *cmd = "MSQ/scan/.eml\n"; uschar av_buffer[1024]; iov[0].iov_base = cmd; iov[0].iov_len = 3; iov[1].iov_base = CS spool_directory; iov[1].iov_len = Ustrlen (spool_directory); iov[2].iov_base = cmd + 3; iov[2].iov_len = 6; iov[3].iov_base = iov[5].iov_base = CS message_id; iov[3].iov_len = iov[5].iov_len = Ustrlen (message_id); iov[4].iov_base = cmd + 3; iov[4].iov_len = 1; iov[6].iov_base = cmd + 9; iov[6].iov_len = 5; if (mksd_writev (sock, iov, 7) < 0) return DEFER; if (mksd_read_lines (sock, av_buffer, sizeof (av_buffer)) < 0) return DEFER; close (sock); return mksd_parse_line (CS av_buffer); } #endif