-/* $Cambridge: exim/src/src/malware.c,v 1.19 2010/06/05 11:13:30 pdp Exp $ */
-
/*************************************************
* Exim - an Internet mail transport agent *
*************************************************/
-/* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003-???? */
+/* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003-2013 */
/* License: GPL */
/* Code for calling virus (malware) scanners. Called from acl.c. */
#include "exim.h"
#ifdef WITH_CONTENT_SCAN
+/* The maximum number of clamd servers that are supported in the configuration */
+#define MAX_CLAMD_SERVERS 32
+#define MAX_CLAMD_SERVERS_S "32"
+/* Maximum length of the hostname that can be specified in the clamd address list */
+#define MAX_CLAMD_ADDRESS_LENGTH 64
+#define MAX_CLAMD_ADDRESS_LENGTH_S "64"
+
+typedef struct clamd_address_container {
+ uschar tcp_addr[MAX_CLAMD_ADDRESS_LENGTH];
+ unsigned int tcp_port;
+} clamd_address_container;
+
/* declaration of private routines */
static int mksd_scan_packed(int sock, uschar *scan_filename);
static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking);
int malware(uschar **listptr) {
uschar scan_filename[1024];
BOOL fits;
+ int ret;
fits = string_format(scan_filename, sizeof(scan_filename),
CS"%s/scan/%s/%s.eml", spool_directory, message_id, message_id);
if (!fits)
{
+ av_failed = TRUE;
log_write(0, LOG_MAIN|LOG_PANIC,
"malware filename does not fit in buffer [malware()]");
return DEFER;
}
- return malware_internal(listptr, scan_filename, FALSE);
+ ret = malware_internal(listptr, scan_filename, FALSE);
+ if (ret == DEFER) av_failed = TRUE;
+
+ return ret;
}
Returns: Exim message processing code (OK, FAIL, DEFER, ...)
where true means malware was found (condition applies)
*/
-int malware_in_file(uschar *eml_filename) {
+int
+malware_in_file(uschar *eml_filename) {
uschar *scan_options[2];
uschar message_id_buf[64];
int ret;
- scan_options[0] = "*";
+ scan_options[0] = US"*";
scan_options[1] = NULL;
/* spool_mbox() assumes various parameters exist, when creating
the relevant directory and the email within */
(void) string_format(message_id_buf, sizeof(message_id_buf),
- US"dummy-%d", pseudo_random_number(INT_MAX));
+ "dummy-%d", vaguely_random_number(INT_MAX));
message_id = message_id_buf;
- sender_address = "malware-sender@example.net";
- return_path = "";
+ sender_address = US"malware-sender@example.net";
+ return_path = US"";
recipients_list = NULL;
- receive_add_recipient("malware-victim@example.net", -1);
+ receive_add_recipient(US"malware-victim@example.net", -1);
enable_dollar_recipients = TRUE;
ret = malware_internal(scan_options, eml_filename, TRUE);
- strncpy(spooled_message_id, message_id, sizeof(spooled_message_id));
+ Ustrncpy(spooled_message_id, message_id, sizeof(spooled_message_id));
spool_mbox_ok = 1;
/* don't set no_mbox_unspool; at present, there's no way for it to become
set, but if that changes, then it should apply to these tests too */
unspool_mbox();
+ /* silence static analysis tools */
+ message_id = NULL;
+
return ret;
}
}
}
else {
- char *drweb_s = NULL;
+ const char *drweb_s = NULL;
if (drweb_rc & DERR_READ_ERR) drweb_s = "read error";
if (drweb_rc & DERR_NOMEMORY) drweb_s = "no memory";
log_write(0, LOG_MAIN|LOG_PANIC,
"malware filename does not fit in buffer [malware_internal() kavdaemon]");
}
- p = strrchr(scanrequest, '/');
+ p = Ustrrchr(scanrequest, '/');
if (p)
*p = '\0';
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);
+ "malware acl condition: regular expression error in '%s': %s at offset %d", cmdline_trigger, rerror, roffset);
return DEFER;
};
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);
+ "malware acl condition: regular expression error in '%s': %s at offset %d", cmdline_regex, rerror, roffset);
return DEFER;
};
"malware filename does not fit in buffer [malware_internal() cmdline]");
return DEFER;
}
- p = strrchr(eml_filename, '/');
+ Ustrcpy(file_name, eml_filename);
+ p = Ustrrchr(file_name, '/');
if (p)
*p = '\0';
fits = string_format(commandline, sizeof(commandline), CS cmdline_scanner, file_name);
struct sockaddr_un server;
int sock, len;
uschar *p;
- BOOL fits;
uschar file_name[1024];
uschar av_buffer[1024];
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);
return DEFER;
}
memcpy(file_name, eml_filename, len);
- p = strrchr(file_name, '/');
+ p = Ustrrchr(file_name, '/');
if (p)
*p = '\0';
DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n",
scanner_name, sophie_options);
- if (write(sock, file_name, Ustrlen(file_name)) < 0) {
+ if ( write(sock, file_name, Ustrlen(file_name)) < 0
+ || write(sock, "\n", 1) != 1
+ ) {
(void)close(sock);
log_write(0, LOG_MAIN|LOG_PANIC,
"malware acl condition: unable to write to sophie UNIX socket (%s)", sophie_options);
return DEFER;
- };
-
- (void)write(sock, "\n", 1);
+ }
/* wait for result */
memset(av_buffer, 0, sizeof(av_buffer));
log_write(0, LOG_MAIN|LOG_PANIC,
"malware acl condition: unable to read from sophie UNIX socket (%s)", sophie_options);
return DEFER;
- };
+ }
(void)close(sock);
else {
/* all ok, no virus */
malware_name = NULL;
- };
+ }
}
/* ----------------------------------------------------------------------- */
* WITH_OLD_CLAMAV_STREAM is defined.
* See Exim bug 926 for details. */
else if (strcmpic(scanner_name,US"clamd") == 0) {
- uschar *clamd_options;
+ uschar *clamd_options = NULL;
uschar clamd_options_buffer[1024];
uschar clamd_options_default[] = "/tmp/clamd";
- uschar *p,*vname;
+ uschar *p, *vname, *result_tag, *response_end;
struct sockaddr_un server;
int sock,bread=0;
unsigned int port;
uschar file_name[1024];
uschar av_buffer[1024];
- uschar hostname[256];
+ uschar *hostname = "";
struct hostent *he;
struct in_addr in;
- uschar *clamd_options2;
- uschar clamd_options2_buffer[1024];
- uschar clamd_options2_default[] = "";
uschar *clamav_fbuf;
- uschar scanrequest[1024];
- int sockData, clam_fd, result;
+ int clam_fd, result;
unsigned int fsize;
- BOOL use_scan_command, fits;
+ BOOL use_scan_command = FALSE, fits;
+ clamd_address_container * clamd_address_vector[MAX_CLAMD_SERVERS];
+ int current_server;
+ int num_servers = 0;
#ifdef WITH_OLD_CLAMAV_STREAM
uschar av_buffer2[1024];
+ int sockData;
#else
uint32_t send_size, send_final_zeroblock;
#endif
/* 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;
- }
- if ((*clamd_options == '/') || (strcmpic(clamd_options2,US"local") == 0))
+ if (*clamd_options == '/')
+ /* Local file; so we def want to use_scan_command and don't want to try
+ * passing IP/port combinations */
use_scan_command = TRUE;
- else
- use_scan_command = FALSE;
+ else {
+ uschar *address = clamd_options;
+ uschar address_buffer[MAX_CLAMD_ADDRESS_LENGTH + 20];
- /* socket does not start with '/' -> network socket */
- if (*clamd_options != '/') {
+ /* Go through the rest of the list of host/port and construct an array
+ * of servers to try. The first one is the bit we just passed from
+ * clamd_options so process that first and then scan the remainder of
+ * the address buffer */
+ do {
+ clamd_address_container *this_clamd;
- /* Confirmed in ClamAV source (0.95.3) that the TCPAddr option of clamd
- * only supports AF_INET, but we should probably be looking to the
- * future and rewriting this to be protocol-independent anyway. */
+ /* The 'local' option means use the SCAN command over the network
+ * socket (ie common file storage in use) */
+ if (strcmpic(address,US"local") == 0) {
+ use_scan_command = TRUE;
+ continue;
+ }
- /* 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;
- };
+ /* XXX: If unsuccessful we should free this memory */
+ this_clamd =
+ (clamd_address_container *)store_get(sizeof(clamd_address_container));
- /* 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;
- }
+ /* extract host and port part */
+ if( sscanf(CS address, "%" MAX_CLAMD_ADDRESS_LENGTH_S "s %u", this_clamd->tcp_addr,
+ &(this_clamd->tcp_port)) != 2 ) {
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "malware acl condition: clamd: invalid address '%s'", address);
+ continue;
+ }
- in = *(struct in_addr *) he->h_addr_list[0];
+ clamd_address_vector[num_servers] = this_clamd;
+ num_servers++;
+ if (num_servers >= MAX_CLAMD_SERVERS) {
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "More than " MAX_CLAMD_SERVERS_S " clamd servers specified; "
+ "only using the first " MAX_CLAMD_SERVERS_S );
+ break;
+ }
+ } while ((address = string_nextinlist(&av_scanner_work, &sep,
+ address_buffer,
+ sizeof(address_buffer))) != NULL);
- /* Open the ClamAV Socket */
- if ( (sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) {
+ /* check if we have at least one server */
+ if (!num_servers) {
log_write(0, LOG_MAIN|LOG_PANIC,
- "malware acl condition: clamd: unable to acquire socket (%s)",
- strerror(errno));
+ "malware acl condition: clamd: no useable clamd server addresses in malware configuration option.");
return DEFER;
}
+ }
- if (ip_connect(sock, AF_INET, (uschar*)inet_ntoa(in), port, 5) < 0) {
- (void)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;
+ /* See the discussion of response formats below to see why we really don't
+ like colons in filenames when passing filenames to ClamAV. */
+ if (use_scan_command && Ustrchr(eml_filename, ':')) {
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "malware acl condition: clamd: local/SCAN mode incompatible with" \
+ " : in path to email filename [%s]", eml_filename);
+ return DEFER;
+ }
+
+ /* We have some network servers specified */
+ if (num_servers) {
+
+ /* Confirmed in ClamAV source (0.95.3) that the TCPAddr option of clamd
+ * only supports AF_INET, but we should probably be looking to the
+ * future and rewriting this to be protocol-independent anyway. */
+
+ while ( num_servers > 0 ) {
+ /* Randomly pick a server to start with */
+ current_server = random_number( num_servers );
+
+ debug_printf("trying server name %s, port %u\n",
+ clamd_address_vector[current_server]->tcp_addr,
+ clamd_address_vector[current_server]->tcp_port);
+
+ /* Lookup the host. This is to ensure that we connect to the same IP
+ * on both connections (as one host could resolve to multiple ips) */
+ if((he = gethostbyname(CS clamd_address_vector[current_server]->tcp_addr))
+ == 0) {
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "malware acl condition: clamd: failed to lookup host '%s'",
+ clamd_address_vector[current_server]->tcp_addr
+ );
+ goto try_next_server;
+ }
+
+ 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));
+ goto try_next_server;
+ }
+
+ if (ip_connect( sock,
+ AF_INET,
+ (uschar*)inet_ntoa(in),
+ clamd_address_vector[current_server]->tcp_port,
+ 5 ) > -1) {
+ /* Connection successfully established with a server */
+ hostname = clamd_address_vector[current_server]->tcp_addr;
+ break;
+ } else {
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "malware acl condition: clamd: connection to %s, port %u failed (%s)",
+ clamd_address_vector[current_server]->tcp_addr,
+ clamd_address_vector[current_server]->tcp_port,
+ strerror(errno));
+
+ (void)close(sock);
+ }
+
+try_next_server:
+ /* Remove the server from the list. XXX We should free the memory */
+ num_servers--;
+ int i;
+ for( i = current_server; i < num_servers; i++ )
+ clamd_address_vector[i] = clamd_address_vector[i+1];
}
+ if ( num_servers == 0 ) {
+ log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: all clamd servers failed");
+ return DEFER;
+ }
} else {
/* open the local socket */
if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
return DEFER;
}
- /* Check the result. ClamAV Returns
- infected: -> "<filename>: <virusname> FOUND"
- not-infected: -> "<filename>: OK"
- error: -> "<filename>: <errcode> ERROR */
+ /* Check the result. ClamAV returns one of two result formats.
+ In the basic mode, the response is of the form:
+ infected: -> "<filename>: <virusname> FOUND"
+ not-infected: -> "<filename>: OK"
+ error: -> "<filename>: <errcode> ERROR
+ If the ExtendedDetectionInfo option has been turned on, then we get:
+ "<filename>: <virusname>(<virushash>:<virussize>) FOUND"
+ for the infected case. Compare:
+/tmp/eicar.com: Eicar-Test-Signature FOUND
+/tmp/eicar.com: Eicar-Test-Signature(44d88612fea8a8f36de82e1278abb02f:68) FOUND
+
+ In the streaming case, clamd uses the filename "stream" which you should
+ be able to verify with { ktrace clamdscan --stream /tmp/eicar.com }. (The
+ client app will replace "stream" with the original filename before returning
+ results to stdout, but the trace shows the data).
+
+ We will assume that the pathname passed to clamd from Exim does not contain
+ a colon. We will have whined loudly above if the eml_filename does (and we're
+ passing a filename to clamd). */
if (!(*av_buffer)) {
log_write(0, LOG_MAIN|LOG_PANIC,
return DEFER;
}
- /* strip newline at the end (won't be present for zINSTREAM) */
+ /* strip newline at the end (won't be present for zINSTREAM)
+ (also any trailing whitespace, which shouldn't exist, but we depend upon
+ this below, so double-check) */
p = av_buffer + Ustrlen(av_buffer) - 1;
- if( *p == '\n' ) *p = '\0';
+ if (*p == '\n') *p = '\0';
DEBUG(D_acl) debug_printf("Malware response: %s\n", av_buffer);
+ while (isspace(*--p) && (p > av_buffer))
+ *p = '\0';
+ if (*p) ++p;
+ response_end = p;
+
/* colon in returned output? */
- if((p = Ustrrchr(av_buffer,':')) == NULL) {
+ if((p = Ustrchr(av_buffer,':')) == NULL) {
log_write(0, LOG_MAIN|LOG_PANIC,
- "malware acl condition: clamd: ClamAV returned malformed result: %s",
+ "malware acl condition: clamd: ClamAV returned malformed result (missing colon): %s",
av_buffer);
return DEFER;
}
/* strip filename */
- ++p;
- while (*p == ' ') ++p;
+ while (*p && isspace(*++p)) /**/;
vname = p;
- 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;
- DEBUG(D_acl) debug_printf("Malware found, name \"%s\"\n", malware_name);
- }
- 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;
- DEBUG(D_acl) debug_printf("Malware not found\n");
- }
+
+ /* It would be bad to encounter a virus with "FOUND" in part of the name,
+ but we should at least be resistant to it. */
+ p = Ustrrchr(vname, ' ');
+ if (p)
+ result_tag = p + 1;
+ else
+ result_tag = vname;
+
+ if (Ustrcmp(result_tag, "FOUND") == 0) {
+ /* p should still be the whitespace before the result_tag */
+ while (isspace(*p)) --p;
+ *++p = '\0';
+ /* Strip off the extended information too, which will be in parens
+ after the virus name, with no intervening whitespace. */
+ if (*--p == ')') {
+ /* "(hash:size)", so previous '(' will do; if not found, we have
+ a curious virus name, but not an error. */
+ p = Ustrrchr(vname, '(');
+ if (p)
+ *p = '\0';
+ }
+ Ustrncpy(malware_name_buffer, vname, sizeof(malware_name_buffer)-1);
+ malware_name = malware_name_buffer;
+ DEBUG(D_acl) debug_printf("Malware found, name \"%s\"\n", malware_name);
+
+ } else if (Ustrcmp(result_tag, "ERROR") == 0) {
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "malware acl condition: clamd: ClamAV returned: %s",
+ av_buffer);
+ return DEFER;
+
+ } else if (Ustrcmp(result_tag, "OK") == 0) {
+ /* Everything should be OK */
+ malware_name = NULL;
+ DEBUG(D_acl) debug_printf("Malware not found\n");
+
+ } else {
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "malware acl condition: clamd: unparseable response from ClamAV: {%s}",
+ av_buffer);
+ return DEFER;
}
- }
+
+ } /* clamd */
+
/* ----------------------------------------------------------------------- */
static int mksd_scan_packed(int sock, uschar *scan_filename)
{
struct iovec iov[3];
- char *cmd = "MSQ\n";
+ const char *cmd = "MSQ\n";
uschar av_buffer[1024];
- iov[0].iov_base = cmd;
+ iov[0].iov_base = (void *) cmd;
iov[0].iov_len = 3;
iov[1].iov_base = CS scan_filename;
iov[1].iov_len = Ustrlen(scan_filename);
- iov[2].iov_base = cmd + 3;
+ iov[2].iov_base = (void *) (cmd + 3);
iov[2].iov_len = 1;
if (mksd_writev (sock, iov, 3) < 0)