This patch changes qmail-smtpd so that it parses incoming emails. It looks at the first line of MIME attachments to see if they're found in control/signatures. This catches nearly all current Microsoft viruses. As of version 1.3, it also cracks open DSN, Exim, and QSBMF bounce messages, and parses the bouncing email. Apply this patch like so: cd /usr/local/src/qmail-1.03 wget http://qmail.org/qmail-smtpd-viruscan-1.3.patch patch /var/qmail/control/signatures # Windows executables seen in active virii TVqQAAMAA TVpQAAIAA # Additional windows executable signatures not yet seen in virii TVpAALQAc TVpyAXkAX TVrmAU4AA TVrhARwAk TVoFAQUAA TVoAAAQAA TVoIARMAA TVouARsAA TVrQAT8AA # .ZIPfile signature seen in SoBig.E and mydoom: UEsDBBQAA UEsDBAoAAA # .GIF file found in a previous Microsoft virus making the rounds. R0lGODlhaAA7APcAAP///+rp6puSp6GZrDUjUUc6Zn53mFJMdbGvvVtXh2xre8bF1x8cU4yLprOy EOF diff -u orig/case_startb.c ./case_startb.c --- orig/case_startb.c 2003-09-25 01:01:48.000000000 -0400 +++ ./case_startb.c 2003-03-06 17:53:59.000000000 -0500 @@ -0,0 +1,21 @@ +#include "case.h" + +int case_startb(s,len,t) +register char *s; +unsigned int len; +register char *t; +{ + register unsigned char x; + register unsigned char y; + + for (;;) { + y = *t++ - 'A'; + if (y <= 'Z' - 'A') y += 'a'; else y += 'A'; + if (!y) return 1; + if (!len) return 0; + --len; + x = *s++ - 'A'; + if (x <= 'Z' - 'A') x += 'a'; else x += 'A'; + if (x != y) return 0; + } +} diff -u orig/Makefile ./Makefile --- orig/Makefile 1998-06-15 06:53:16.000000000 -0400 +++ ./Makefile 2003-03-10 10:49:44.000000000 -0500 @@ -217,9 +217,9 @@ case.a: \ makelib case_diffb.o case_diffs.o case_lowerb.o case_lowers.o \ -case_starts.o +case_starts.o case_startb.o ./makelib case.a case_diffb.o case_diffs.o case_lowerb.o \ - case_lowers.o case_starts.o + case_lowers.o case_starts.o case_startb.o case_diffb.o: \ compile case_diffb.c case.h @@ -237,6 +237,10 @@ compile case_lowers.c case.h ./compile case_lowers.c +case_startb.o: \ +compile case_startb.c case.h + ./compile case_startb.c + case_starts.o: \ compile case_starts.c case.h ./compile case_starts.c diff -u orig/qmail-smtpd.c ./qmail-smtpd.c --- orig/qmail-smtpd.c 1998-06-15 06:53:16.000000000 -0400 +++ ./qmail-smtpd.c 2004-01-27 16:54:24.000000000 -0500 @@ -96,6 +96,8 @@ int bmfok = 0; stralloc bmf = {0}; struct constmap mapbmf; +int sigsok = 0; +stralloc sigs = {0}; void setup() { @@ -117,6 +119,9 @@ if (bmfok) if (!constmap_init(&mapbmf,bmf.s,bmf.len,0)) die_nomem(); + sigsok = control_readfile(&sigs,"control/signatures",0); + if (sigsok == -1) die_control(); + if (control_readint(&databytes,"control/databytes") == -1) die_control(); x = env_get("DATABYTES"); if (x) { scan_ulong(x,&u); databytes = u; } @@ -208,6 +213,19 @@ return 0; } +int sigscheck(stralloc *line) { + int i, j; + + j = 0; + for (i = 0; i < sigs.len; i++) if (!sigs.s[i]) { + if (i-j < line->len) + if (!str_diffn(line->s,sigs.s+j,i-j)) + return 1; + j = i+1; + } + return 0; +} + int addrallowed() { int r; @@ -281,9 +299,147 @@ struct qmail qqt; unsigned int bytestooverflow = 0; +int linespastheader; /* =0 after boundary is found in body, */ + /* until blank line */ +char linetype; +int flagexecutable; +int flagqsbmf; + +stralloc line = {0}; +stralloc content = {0}; +stralloc boundary = {0}; +int boundary_start; + +/* + +def put(ch): + line.append(ch) + if ch == '\n': + if linepastheader == 0: + if line.startswith('Content-Type:'): + content = + + put() puts characters into the queue. We remember those characters + and form them into a line. When we get a newline, we examine the + line. If we're currently in a header (0 linespastheader), we look + for Content-Type. If we're at the newline that ends a header, we + look to see if the content is multipart. If it is, then we push + the current boundary, remember the boundary, otherwise we set the + boundary to the empty string. Set the linespastheader to 1. When + linespastheader is 1, and the boundary is empty, scan the line for + signatures. If the boundary is non-empty, look for a match against + the boundary. If it matches and is followed by two dashes, pop the + boundary, otherwise set linespastheader to 0. +*/ + void put(ch) char *ch; { + char *cp, *cpstart, *cpafter; + unsigned int len; + + if (line.len < 1024) + if (!stralloc_catb(&line,ch,1)) die_nomem(); + + if (*ch == '\n') { + if (linespastheader == 0) { + if (line.len == 1) { + linespastheader = 1; + if (flagqsbmf) { + flagqsbmf = 0; + linespastheader = 0; + } + if (content.len) { /* MIME header */ + cp = content.s; + len = content.len; + while (len && (*cp == ' ' || *cp == '\t')) { ++cp; --len; } + cpstart = cp; + if (len && *cp == '"') { /* might be commented */ + ++cp; --len; cpstart = cp; + while (len && *cp != '"') { ++cp; --len; } + } else { + while (len && *cp != ' ' && *cp != '\t' && *cp != ';') { + ++cp; --len; + } + } + if (!case_diffb(cpstart,cp-cpstart,"message/rfc822")) + linespastheader = 0; + + cpafter = content.s+content.len; + while((cp += byte_chr(cp,cpafter-cp,';')) != cpafter) { + ++cp; + while (cp < cpafter && (*cp == ' ' || *cp == '\t')) ++cp; + if (case_startb(cp,cpafter - cp,"boundary=")) { + cp += 9; /* after boundary= */ + if (cp < cpafter && *cp == '"') { + ++cp; + cpstart = cp; + while (cp < cpafter && *cp != '"') ++cp; + } else { + cpstart = cp; + while (cp < cpafter && + *cp != ';' && *cp != ' ' && *cp != '\t') ++cp; + } + /* push the current boundary. Append a null and remember start. */ + if (!stralloc_0(&boundary)) die_nomem(); + boundary_start = boundary.len; + if (!stralloc_cats(&boundary,"--")) die_nomem(); + if (!stralloc_catb(&boundary,cpstart,cp-cpstart)) + die_nomem(); + break; + } + } + } + } else { /* non-blank header line */ + if ((*line.s == ' ' || *line.s == '\t')) { + switch(linetype) { + case 'C': if (!stralloc_catb(&content,line.s,line.len-1)) die_nomem(); break; + default: break; + } + } else { + if (case_startb(line.s,line.len,"content-type:")) { + if (!stralloc_copyb(&content,line.s+13,line.len-14)) die_nomem(); + linetype = 'C'; + } else { + linetype = ' '; + } + } + } + } else { /* non-header line */ + if (boundary.len-boundary_start && *line.s == '-' && line.len > (boundary.len-boundary_start) && + !str_diffn(line.s,boundary.s+boundary_start,boundary.len-boundary_start)) { /* matches a boundary */ + if (line.len > boundary.len-boundary_start + 2 && + line.s[boundary.len-boundary_start+0] == '-' && + line.s[boundary.len-boundary_start+1] == '-') { + /* XXXX - pop the boundary here */ + if (boundary_start) boundary.len = boundary_start - 1; + boundary_start = boundary.len; + while(boundary_start--) if (!boundary.s[boundary_start]) break; + boundary_start++; + linespastheader = 2; + } else { + linespastheader = 0; + } + } else if (linespastheader == 1) { /* first line -- match a signature? */ + if (/*mailfrom.s[0] == '\0' && */ + str_start(line.s,"Hi. This is the ")) + flagqsbmf = 1; + else if (/*mailfrom.s[0] == '\0' && */ + str_start(line.s,"This message was created automatically by mail delivery software")) + flagqsbmf = 1; + else if (sigscheck(&line)) { + flagexecutable = 1; + qmail_fail(&qqt); + } + linespastheader = 2; + } + if (flagqsbmf && str_start(line.s,"---")) { + linespastheader = 0; + } + } + line.len = 0; + } + if (bytestooverflow) if (!--bytestooverflow) qmail_fail(&qqt); @@ -374,6 +530,13 @@ if (!rcptto.len) { err_wantrcpt(); return; } seenmail = 0; if (databytes) bytestooverflow = databytes + 1; + boundary.len = 0; + boundary_start = 0; + content.len = 0; + linespastheader = 0; + flagexecutable = 0; + flagqsbmf = 0; + linetype = ' '; if (qmail_open(&qqt) == -1) { err_qqt(); return; } qp = qmail_qp(&qqt); out("354 go ahead\r\n"); @@ -389,6 +552,7 @@ if (!*qqx) { acceptmessage(qp); return; } if (hops) { out("554 too many hops, this message is looping (#5.4.6)\r\n"); return; } if (databytes) if (!bytestooverflow) { out("552 sorry, that message size exceeds my databytes limit (#5.3.4)\r\n"); return; } + if (flagexecutable) { out("552 we don't accept email with such content (#5.3.4)\r\n"); return; } if (*qqx == 'D') out("554 "); else out("451 "); out(qqx + 1); out("\r\n");