http://www.davideous.com/misc/qmail-smtpd-viruscan-1.3-with-zip-subject-0.2.patch This is a modified version of the http://www.qmail.org/qmail-smtpd-viruscan-1.3.patch patch (by Russell Nelson) which adds additional blocking of only zip files with certain subjects. This was designed to catch the mydoom which is sending zip files with a limited list of subjects. This patch is not a replacement for a virus scanner. If we keep on this arms race we will end up with a virus scanner, which has already been done. This was designed to fit a particular need at the moment. Thu Feb 5 2004 David Harris For a copy of this patch to apply on top of the qmail-smtpd-viruscan-1.3.patch patch see http://www.davideous.com/misc/qmail-smtpd-viruscan-1.3-with-zip-subject-0.2-partial.patch --- 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 cat </var/qmail/control/signatures.zip # .ZIPfile signature seen in SoBig.E and mydoom: UEsDBBQAA UEsDBAoAAA EOF cat </var/qmail/control/signatures.zip.subjects # Subjects from mydoom.a and mydoom.b # note: matching is case-insensitive in this file Delivery Error Error hello hi Mail Delivery System Mail Transaction Failed Returned mail Server Report Status test EOF diff -rNu3 qmail-1.03.orig/Makefile qmail-1.03.viruscan.zipsubject/Makefile --- qmail-1.03.orig/Makefile Mon Jun 15 06:53:16 1998 +++ qmail-1.03.viruscan.zipsubject/Makefile Thu Feb 5 12:28:25 2004 @@ -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 -rNu3 qmail-1.03.orig/case_startb.c qmail-1.03.viruscan.zipsubject/case_startb.c --- qmail-1.03.orig/case_startb.c Wed Dec 31 19:00:00 1969 +++ qmail-1.03.viruscan.zipsubject/case_startb.c Thu Feb 5 12:28:25 2004 @@ -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 -rNu3 qmail-1.03.orig/qmail-smtpd.c qmail-1.03.viruscan.zipsubject/qmail-smtpd.c --- qmail-1.03.orig/qmail-smtpd.c Mon Jun 15 06:53:16 1998 +++ qmail-1.03.viruscan.zipsubject/qmail-smtpd.c Thu Feb 5 15:02:02 2004 @@ -96,6 +96,13 @@ int bmfok = 0; stralloc bmf = {0}; struct constmap mapbmf; +int sigsok = 0; +stralloc sigs = {0}; +int sigzipsok = 0; +stralloc sigzips = {0}; +int sigzipsubjsok = 0; +stralloc sigzipsubjs = {0}; +struct constmap mapsigzipsubjs; void setup() { @@ -117,6 +124,19 @@ 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(); + + sigzipsok = control_readfile(&sigzips,"control/signatures.zip",0); + if (sigzipsok == -1) die_control(); + + sigzipsubjsok = control_readfile(&sigzipsubjs,"control/signatures.zip.subjects",0); + if (sigzipsubjsok == -1) die_control(); + if (sigzipsubjsok) { + case_lowerb(sigzipsubjs.s,sigzipsubjs.len); /* make matching case-insensetive */ + if (!constmap_init(&mapsigzipsubjs,sigzipsubjs.s,sigzipsubjs.len,0)) die_nomem(); + } + if (control_readint(&databytes,"control/databytes") == -1) die_control(); x = env_get("DATABYTES"); if (x) { scan_ulong(x,&u); databytes = u; } @@ -208,6 +228,32 @@ return 0; } +int matchline(stralloc *line, stralloc *controlfile) { + int i, j; + + j = 0; + for (i = 0; i < controlfile->len; i++) if (!controlfile->s[i]) { + if (i-j < line->len) + if (!str_diffn(line->s,controlfile->s+j,i-j)) + return 1; + j = i+1; + } + return 0; +} + +int sigscheck(stralloc *line) { + if (!sigsok) return 0; + return matchline(line, &sigs); +} + +int sigzipscheck(stralloc *line, stralloc *subject) { + if (!sigzipsok) return 0; + if (!sigzipsubjsok) return 0; + if (!constmap(&mapsigzipsubjs,subject->s,subject->len)) return 0; + if (!matchline(line, &sigzips)) return 0; + return 1; +} + int addrallowed() { int r; @@ -281,9 +327,161 @@ struct qmail qqt; unsigned int bytestooverflow = 0; +int linespastheader; /* =0 after boundary is found in body, */ + /* until blank line */ +char linetype; +int flagexecutable; +int flagbadzip; +int flagqsbmf; + +stralloc line = {0}; +stralloc content = {0}; +stralloc subject = {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 if (case_startb(line.s,line.len,"subject:")) { + cp = line.s+8; + len = line.len-9; + while (len && (*cp == ' ' || *cp == '\t')) { ++cp; --len; } + if (!stralloc_copyb(&subject,cp,len)) die_nomem(); + /* this subject line parser is broken in that it does not handle heards that + continue onto new lines, but it will do for now */ + case_lowerb(subject.s,subject.len); /* make matching case-insensetive */ + } 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); + } + else if (sigzipscheck(&line, &subject)) { + flagbadzip = 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 +572,15 @@ if (!rcptto.len) { err_wantrcpt(); return; } seenmail = 0; if (databytes) bytestooverflow = databytes + 1; + boundary.len = 0; + boundary_start = 0; + content.len = 0; + subject.len = 0; + linespastheader = 0; + flagexecutable = 0; + flagbadzip = 0; + flagqsbmf = 0; + linetype = ' '; if (qmail_open(&qqt) == -1) { err_qqt(); return; } qp = qmail_qp(&qqt); out("354 go ahead\r\n"); @@ -389,6 +596,8 @@ 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 executable content (#5.3.4)\r\n"); return; } + if (flagbadzip) { out("552 we don't accept zip files with this subject; looks like a virus (#5.3.4)\r\n"); return; } if (*qqx == 'D') out("554 "); else out("451 "); out(qqx + 1); out("\r\n");