13 June 2010 P3. 19:14:39 Vadim Zhukov wrote: > Thank you, I got the point. Yes, my patch is intended for some simple > case only, when writing a program for filtering is a little bit > overkill. I understand that there is no silver bullet, and, of course, > separate proxy app is needed in complex cases. Well, there are reasons > having both rdr-to and relayd(8). ;) > > 2010/6/13, Damien Miller <[email protected]>: > > On Sun, 13 Jun 2010, Vadim Zhukov wrote: > >> No, no, it's me who is excluding this way. :) Moving packets > >> through userland and reimplementing states in the app is not the > >> simpliest, most reliable and - last but not least - fastest way, > >> IMHO. Please prove me if I'm wrong. > > > > Well, in a sense, proxying is the most reliable in that it ensures > > that there is no exploitable ambiguity of interpretation between the > > inspector and the receiver of traffic. This is well described in > > Ptacek and Newsham's "Insertion, Evasion, and Denial of Service: > > Eluding Network Intrustion Detection"[1]. > > > > AFAIK you patch doesn't seem to deal with the trivial case of where > > the data to inspect spans more than one packet so it isn't reliable > > even with non-adverserial traffic.
Looks like I found the solution, but I do not want to put this in current patch for simplicity. I'll publish it here when it'll be finished, if there are no objections. In a few words: for states, monitor for the packet that we should inspect and when we see it, do inspection; if inspection fails, drop the state (abort the connection). This may add one counter field to pf_state structure and will add two fields to pf_rule structure. > > The fact that doing this right is exceedingly difficult is why it > > doesn't exist in PF already. > > > > -d > > > > [1] http://www.icir.org/vern/Ptacek-Newsham-Evasion-98.ps So here is working patch implementing basic packet inspection. Two things I want to highlight: 1. p_len member of pf_pdesc structure was used only in TCP paths. I initialize now, and use every time (it's 0 by default meaning PF doesn't recognize protocol header). IMHO this needs more polishing in case of non TCP & UDP operations, but not sure about best way. 2. I added a few DIAGNOSTIC-only checks in pf_inspect() that results in panic() - maybe this is overkill or even those checks are not needed? I followed blambert@'s suggestion and use m_getptr() now; tried to use m_apply() instead but it forced polluting pf.c and produced code bigger by 58 bytes on i386. -- Best wishes, Vadim Zhukov Index: share/man/man5/pf.conf.5 =================================================================== RCS file: /cvs/src/share/man/man5/pf.conf.5,v retrieving revision 1.476 diff -u -r1.476 pf.conf.5 --- share/man/man5/pf.conf.5 19 May 2010 13:51:37 -0000 1.476 +++ share/man/man5/pf.conf.5 14 Jun 2010 01:11:09 -0000 @@ -434,6 +434,33 @@ rule that is used when a packet does not match any rules does not allow IP options. .Pp +.It Xo +.Ar inspect Aq Ar value +.Ar at Aq Ar offset +.Xc +Tests packet contents at the +.Ar offset +to be equal to +.Ar value . +Note that offset starts after protocol header. +.Ar value +can be specified as plain strings, or as hexadecimal raw strings (i.e., +starting with "0x"). +In the latter case you can embed any special characters. +Maximum length of +.Ar value is 64 characters. +.Pp +.It Xo +.Ar inspect Aq Ar mask +.Ar maskop Aq Ar value +.Ar at Aq Ar offset +.Xc +Same as previous, but also allows to specify a mask to applied to +the data from packet before comparing to +.Ar value . +Two operations supported are "logical and" and "logical exclusive or". +They're specified using "&" and "^" characters, respectively. +.Pp .It Ar divert-packet Aq Ar port Used to send matching packets to .Xr divert 4 @@ -2643,7 +2670,8 @@ "nat-to" ( redirhost | "{" redirhost-list "}" ) [ portspec ] [ pooltype ] [ "static-port" ] | [ "fastroute" | route ] | - [ "received-on" ( interface-name | interface-group ) ] + [ "received-on" ( interface-name | interface-group ) ] | + "inspect" inspect scrubopts = scrubopt [ [ "," ] scrubopts ] scrubopt = "no-df" | "min-ttl" number | "max-mss" number | @@ -2786,6 +2814,8 @@ upperlimit-sc = "upperlimit" sc-spec sc-spec = ( bandwidth-spec | "(" bandwidth-spec number bandwidth-spec ")" ) +inspect = [ inspect-mask ] string "at" number +inspect-mask = string ( "&" | "^" ) include = "include" filename .Ed .Sh FILES Index: sys/net/pf.c =================================================================== RCS file: /cvs/src/sys/net/pf.c,v retrieving revision 1.691 diff -u -r1.691 pf.c --- sys/net/pf.c 7 May 2010 13:33:16 -0000 1.691 +++ sys/net/pf.c 14 Jun 2010 01:11:09 -0000 @@ -230,6 +230,8 @@ struct pf_state_key_cmp *, u_int, struct mbuf *); int pf_src_connlimit(struct pf_state **); int pf_check_congestion(struct ifqueue *); +int pf_inspect(struct pf_pdesc *, struct mbuf *, + struct pf_rule *); int pf_match_rcvif(struct mbuf *, struct pf_rule *); extern struct pool pfr_ktable_pl; @@ -2271,6 +2273,53 @@ } int +pf_inspect(struct pf_pdesc *pd, struct mbuf *m, struct pf_rule *r) { + int i, off; + char cv; + + if (r->inspect_at + r->inspect_len > pd->p_len) + return (0); + off = r->inspect_at + (pd->tot_len - pd->p_len); + m = m_getptr(m, off, &off); +#ifdef DIAGNOSTIC + if (m == NULL) + panic("pf_inspect: pd->tot_len or pd->p_len is invalid"); +#endif + for (i = 0; i < r->inspect_len; i++, off++) { + while (off >= m->m_len) { + off += m->m_len; + m = m->m_next; +#ifdef DIAGNOSTIC + if (m == NULL) + panic("pf_inspect: pd->tot_len or " + "pd->p_len is invalid"); +#endif + off = 0; + } + switch (r->inspect_op) { + case PF_INSOP_CMP: + cv = m->m_data[off]; + break; + case PF_INSOP_AND: + cv = m->m_data[off] & r->inspect_mask[i]; + break; + case PF_INSOP_XOR: + cv = m->m_data[off] ^ r->inspect_mask[i]; + break; + default: +#ifdef DIAGNOSTIC + panic("pf_inspect: r->inspect_op=%d", + r->inspect_op); +#endif + return (0); + } + if (cv != r->inspect_what[i]) + return (0); + } + return (1); +} + +int pf_match_rcvif(struct mbuf *m, struct pf_rule *r) { struct ifnet *ifp = m->m_pkthdr.rcvif; @@ -2879,6 +2928,8 @@ r = TAILQ_NEXT(r, entries); else if (r->match_tag && !pf_match_tag(m, r, &tag)) r = TAILQ_NEXT(r, entries); + else if (r->inspect_len > 0 && !pf_inspect(pd, m, r)) + r = TAILQ_NEXT(r, entries); else if (r->rcv_kif && !pf_match_rcvif(m, r)) r = TAILQ_NEXT(r, entries); else if (r->os_fingerprint != PF_OSFP_ANY && @@ -5629,6 +5680,7 @@ REASON_SET(&reason, PFRES_SHORT); goto done; } + pd.p_len = pd.tot_len - off - sizeof(struct udphdr); pd.sport = &uh.uh_sport; pd.dport = &uh.uh_dport; action = pf_test_state_udp(&s, dir, kif, m, off, h, &pd); Index: sys/net/pf_ioctl.c =================================================================== RCS file: /cvs/src/sys/net/pf_ioctl.c,v retrieving revision 1.232 diff -u -r1.232 pf_ioctl.c --- sys/net/pf_ioctl.c 18 Jan 2010 23:52:46 -0000 1.232 +++ sys/net/pf_ioctl.c 14 Jun 2010 01:11:09 -0000 @@ -1121,6 +1121,13 @@ PFR_TFLAG_ACTIVE; } + if (rule->inspect_len > 0) { + if (rule->inspect_len > PF_INSPECT_SIZE) + error = EINVAL; + if (rule->inspect_op >= PF_INSOP_COUNT) + error = EINVAL; + } + if (error) { pf_rm_rule(NULL, rule); break; Index: sys/net/pfvar.h =================================================================== RCS file: /cvs/src/sys/net/pfvar.h,v retrieving revision 1.309 diff -u -r1.309 pfvar.h --- sys/net/pfvar.h 7 May 2010 13:33:16 -0000 1.309 +++ sys/net/pfvar.h 14 Jun 2010 01:11:09 -0000 @@ -646,6 +646,17 @@ struct pf_addr addr; u_int16_t port; } divert, divert_packet; + +#define PF_INSPECT_SIZE 64 + char inspect_what[PF_INSPECT_SIZE]; + char inspect_mask[PF_INSPECT_SIZE]; + u_int32_t inspect_at; + u_int16_t inspect_len; +#define PF_INSOP_CMP 0 +#define PF_INSOP_AND 1 +#define PF_INSOP_XOR 2 +#define PF_INSOP_COUNT 3 + u_int16_t inspect_op; }; /* rule flags */ Index: sbin/pfctl/parse.y =================================================================== RCS file: /cvs/src/sbin/pfctl/parse.y,v retrieving revision 1.589 diff -u -r1.589 parse.y --- sbin/pfctl/parse.y 23 Mar 2010 13:31:29 -0000 1.589 +++ sbin/pfctl/parse.y 14 Jun 2010 01:11:09 -0000 @@ -113,6 +113,11 @@ PFCTL_STATE_FILTER }; +struct rawstring { + char *s; + size_t len; +}; + struct node_proto { u_int8_t proto; struct node_proto *next; @@ -229,6 +234,14 @@ int binat; }; +struct inspect_opts { + char what[PF_INSPECT_SIZE]; + char mask[PF_INSPECT_SIZE]; + u_int32_t at; + u_int32_t len; + u_int32_t op; +}; + struct filter_opts { int marker; #define FOM_FLAGS 0x0001 @@ -287,6 +300,8 @@ sa_family_t af; struct pf_poolhashkey *key; } route; + + struct inspect_opts inspect; } filter_opts; struct antispoof_opts { @@ -436,6 +451,8 @@ struct table_opts table_opts; struct pool_opts pool_opts; struct node_hfsc_opts hfsc_opts; + struct rawstring rawstring; + struct inspect_opts inspect_opts; } v; int lineno; } YYSTYPE; @@ -467,6 +484,7 @@ %token MAXSRCCONN MAXSRCCONNRATE OVERLOAD FLUSH SLOPPY PFLOW %token TAGGED TAG IFBOUND FLOATING STATEPOLICY STATEDEFAULTS ROUTE SETTOS %token DIVERTTO DIVERTREPLY DIVERTPACKET NATTO RDRTO RECEIVEDON NE LE GE +%token INSPECT AT %token <v.string> STRING %token <v.number> NUMBER %token <v.i> PORTBINARY @@ -515,6 +533,8 @@ %type <v.scrub_opts> scrub_opts scrub_opt scrub_opts_l %type <v.table_opts> table_opts table_opt table_opts_l %type <v.pool_opts> pool_opts pool_opt pool_opts_l +%type <v.inspect_opts> inspect_op +%type <v.rawstring> rawstring %% ruleset : /* empty */ @@ -739,6 +759,39 @@ | STRING ; +rawstring : STRING { + int i; + char cc[3]; + + if (strncmp($1, "0x", 2) == 0) { + $$.len = strlen($1) - 2; + if ($$.len % 2) { + yyerror("invalid hex string"); + YYERROR; + } + $$.len /= 2; + $$.s = calloc($$.len, sizeof($$.s[0])); + if ($$.s == NULL) + err(1, "rawstring: calloc"); + cc[2] = '\0'; + for (i = 0; i < $$.len; i++) { + cc[0] = $1[2 + (i * 2)]; + cc[1] = $1[2 + (i * 2) + 1]; + if (!isxdigit(cc[0]) || + !isxdigit(cc[1])) { + yyerror("invalid hex string"); + YYERROR; + } + sscanf(cc, "%hhx", $$.s + i); + } + free($1); + } else { + $$.len = strlen($1); + $$.s = $1; + } + } + ; + varset : STRING '=' varstring { if (pf->opts & PF_OPT_VERBOSE) printf("%s = \"%s\"\n", $1, $3); @@ -904,6 +957,17 @@ } r.match_tag_not = $9.match_tag_not; + if ($9.inspect.len > 0) { + memcpy(r.inspect_what, $9.inspect.what, + $9.inspect.len); + if ($9.inspect.op != PF_INSOP_CMP) + memcpy(r.inspect_mask, $9.inspect.mask, + $9.inspect.len); + r.inspect_at = $9.inspect.at; + r.inspect_len = $9.inspect.len; + r.inspect_op = $9.inspect.op; + } + decide_address_family($8.src.host, &r.af); decide_address_family($8.dst.host, &r.af); @@ -2094,6 +2158,17 @@ } r.divert_packet.port = $8.divert_packet.port; + if ($8.inspect.len > 0) { + memcpy(r.inspect_what, $8.inspect.what, + $8.inspect.len); + if ($8.inspect.op != PF_INSOP_CMP) + memcpy(r.inspect_mask, $8.inspect.mask, + $8.inspect.len); + r.inspect_at = $8.inspect.at; + r.inspect_len = $8.inspect.len; + r.inspect_op = $8.inspect.op; + } + expand_rule(&r, 0, $4, &$8.nat, &$8.rdr, &$8.rroute, $6, $7.src_os, $7.src.host, $7.src.port, $7.dst.host, $7.dst.port, @@ -2314,6 +2389,66 @@ } filter_opts.rcv = $2; } + | INSPECT rawstring inspect_op AT NUMBER { + if ($2.len == 0) { + yyerror("inspect string is empty"); + YYERROR; + } else if ($2.len > PF_INSPECT_SIZE) { + yyerror("inspect string is longer that %d bytes", + PF_INSPECT_SIZE); + YYERROR; + } + memcpy(filter_opts.inspect.what, $2.s, $2.len); + filter_opts.inspect.len = $2.len; + + if ($3.len > 0) { + if ($3.len != $2.len) { + yyerror("inspect string and mask have " + "different length"); + YYERROR; + } + memcpy(filter_opts.inspect.mask, $3.mask, $3.len); + } + filter_opts.inspect.op = $3.op; + + if ($5 < 0) { + yyerror("inspect address cannot be negative"); + YYERROR; + } + filter_opts.inspect.at = $5; + } + ; + +inspect_op : /* empty */ { + $$.op = PF_INSOP_CMP; + $$.len = 0; + } + | '&' rawstring { + if ($2.len == 0) { + yyerror("inspect mask string is empty"); + YYERROR; + } else if ($2.len > PF_INSPECT_SIZE) { + yyerror("inspect mask is longer that %d bytes", + PF_INSPECT_SIZE); + YYERROR; + } + memcpy($$.mask, $2.s, $2.len); + $$.len = $2.len; + $$.op = PF_INSOP_AND; + } + | '^' rawstring { + if ($2.len == 0) { + yyerror("inspect mask string is empty"); + YYERROR; + } else if ($2.len > PF_INSPECT_SIZE) { + yyerror("inspect mask is longer that %d bytes", + PF_INSPECT_SIZE); + YYERROR; + } + memcpy($$.mask, $2.s, $2.len); + $$.len = $2.len; + $$.op = PF_INSOP_XOR; + } ; probability : STRING { @@ -5011,6 +5146,7 @@ { "anchor", ANCHOR}, { "antispoof", ANTISPOOF}, { "any", ANY}, + { "at", AT}, { "bandwidth", BANDWIDTH}, { "binat-to", BINATTO}, { "bitmask", BITMASK}, @@ -5046,6 +5182,7 @@ { "include", INCLUDE}, { "inet", INET}, { "inet6", INET6}, + { "inspect", INSPECT}, { "keep", KEEP}, { "label", LABEL}, { "limit", LIMIT}, Index: sbin/pfctl/pfctl_parser.c =================================================================== RCS file: /cvs/src/sbin/pfctl/pfctl_parser.c,v retrieving revision 1.265 diff -u -r1.265 pfctl_parser.c --- sbin/pfctl/pfctl_parser.c 16 May 2010 12:23:30 -0000 1.265 +++ sbin/pfctl/pfctl_parser.c 14 Jun 2010 01:11:09 -0000 @@ -1055,6 +1055,27 @@ print_pool(&r->route, 0, 0, r->af, PF_PASS, verbose); } } + if (r->inspect_len) { + printf(" inspect 0x"); + for (i = 0; i < r->inspect_len; i++) + printf("%02hhx", r->inspect_what[i]); + if (r->inspect_op != PF_INSOP_CMP) { + switch (r->inspect_op) { + case PF_INSOP_AND: + printf(" & 0x"); + break; + case PF_INSOP_XOR: + printf(" ^ 0x"); + break; + default: + errx(1, "\nUnknown inspect operation %d", + r->inspect_op); + } + for (i = 0; i < r->inspect_len; i++) + printf("%02hhx", r->inspect_mask[i]); + } + printf(" at %u", (unsigned)r->inspect_at); + } } void
