Kconfig | 14 + Makefile | 1 ipt_unclean.c | 612 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 627 insertions(+) diff -Nur --exclude '*.orig' linux.org/net/ipv4/netfilter/Kconfig linux/net/ipv4/netfilter/Kconfig --- linux.org/net/ipv4/netfilter/Kconfig 2006-05-02 23:38:44.000000000 +0200 +++ linux/net/ipv4/netfilter/Kconfig 2006-05-04 14:49:54.000000000 +0200 @@ -606,5 +606,19 @@ Allows altering the ARP packet payload: source and destination hardware and network addresses. +config IP_NF_MATCH_UNCLEAN + tristate 'Unclean match support (DANGEROUS)' + depends on EXPERIMENTAL && IP_NF_IPTABLES + help + Unclean packet matching matches any strange or invalid packets, by + looking at a series of fields in the IP, TCP, UDP and ICMP headers. + + Please note that this kind of matching is considered dangerous and + might harm the future compatibility of your packet filter. + + It has happened before, search on the net for ECN blackholes :( + + + endmenu diff -Nur --exclude '*.orig' linux.org/net/ipv4/netfilter/Makefile linux/net/ipv4/netfilter/Makefile --- linux.org/net/ipv4/netfilter/Makefile 2006-05-02 23:38:44.000000000 +0200 +++ linux/net/ipv4/netfilter/Makefile 2006-05-04 14:49:54.000000000 +0200 @@ -0,0 +0,1 @@ +obj-$(CONFIG_IP_NF_MATCH_UNCLEAN) += ipt_unclean.o diff -Nur --exclude '*.orig' linux.org/net/ipv4/netfilter/ipt_unclean.c linux/net/ipv4/netfilter/ipt_unclean.c --- linux.org/net/ipv4/netfilter/ipt_unclean.c 1970-01-01 01:00:00.000000000 +0100 +++ linux/net/ipv4/netfilter/ipt_unclean.c 2006-05-04 14:49:54.000000000 +0200 @@ -0,0 +1,612 @@ +/* Kernel module to match suspect packets. */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define limpk(format, args...) \ +do { \ + if (net_ratelimit()) \ + printk("ipt_unclean: %s" format, \ + embedded ? "(embedded packet) " : "" , ## args); \ +} while(0) + +enum icmp_error_status +{ + ICMP_MAY_BE_ERROR, + ICMP_IS_ERROR, + ICMP_NOT_ERROR +}; + +struct icmp_info +{ + size_t min_len, max_len; + enum icmp_error_status err; + u_int8_t min_code, max_code; +}; + +static int +check_ip(const struct sk_buff *skb, unsigned int offset); + +/* ICMP-specific checks. */ +static int +check_icmp(const struct sk_buff *skb, + unsigned int offset, + unsigned int fragoff, + int more_frags, + int embedded) +{ + struct icmphdr icmph; + static struct icmp_info info[] + = { [ICMP_ECHOREPLY] + = { 8, 65536, ICMP_NOT_ERROR, 0, 0 }, + [ICMP_DEST_UNREACH] + = { 8 + 28, 65536, ICMP_IS_ERROR, 0, 15 }, + [ICMP_SOURCE_QUENCH] + = { 8 + 28, 65536, ICMP_IS_ERROR, 0, 0 }, + [ICMP_REDIRECT] + = { 8 + 28, 65536, ICMP_IS_ERROR, 0, 3 }, + [ICMP_ECHO] + = { 8, 65536, ICMP_NOT_ERROR, 0, 0 }, + /* Router advertisement. */ + [9] + = { 8, 8 + 255 * 8, ICMP_NOT_ERROR, 0, 0 }, + /* Router solicitation. */ + [10] + = { 8, 8, ICMP_NOT_ERROR, 0, 0 }, + [ICMP_TIME_EXCEEDED] + = { 8 + 28, 65536, ICMP_IS_ERROR, 0, 1 }, + [ICMP_PARAMETERPROB] + = { 8 + 28, 65536, ICMP_IS_ERROR, 0, 1 }, + [ICMP_TIMESTAMP] + = { 20, 20, ICMP_NOT_ERROR, 0, 0 }, + [ICMP_TIMESTAMPREPLY] + = { 20, 20, ICMP_NOT_ERROR, 0, 0 }, + [ICMP_INFO_REQUEST] + = { 8, 65536, ICMP_NOT_ERROR, 0, 0 }, + [ICMP_INFO_REPLY] + = { 8, 65536, ICMP_NOT_ERROR, 0, 0 }, + [ICMP_ADDRESS] + = { 12, 12, ICMP_NOT_ERROR, 0, 0 }, + [ICMP_ADDRESSREPLY] + = { 12, 12, ICMP_NOT_ERROR, 0, 0 } }; + + /* Can't do anything if it's a fragment. */ + if (fragoff) + return 1; + + /* CHECK: Must have whole header.. */ + if (skb_copy_bits(skb, offset, &icmph, sizeof(icmph)) < 0) { + limpk("ICMP len=%u too short\n", skb->len - offset); + return 0; + } + + /* If not embedded in an ICMP error already. */ + if (!embedded) { + /* CHECK: Truncated ICMP (even if first fragment). */ + if (icmph.type < sizeof(info)/sizeof(struct icmp_info) + && info[icmph.type].min_len != 0 + && skb->len - offset < info[icmph.type].min_len) { + limpk("ICMP type %u len %u too short\n", + icmph.type, skb->len - offset); + return 0; + } + + /* CHECK: Check within known error ICMPs. */ + if (icmph.type < sizeof(info)/sizeof(struct icmp_info) + && info[icmph.type].err == ICMP_IS_ERROR) { + /* Max IP header size = 60 */ + char inner[60 + 8]; + struct iphdr *inner_ip = (struct iphdr *)inner; + + /* CHECK: Embedded packet must be at least + length of iph + 8 bytes. */ + if (skb_copy_bits(skb, offset + sizeof(icmph), + inner, sizeof(struct iphdr)+8) < 0) { + limpk("ICMP error internal way too short\n"); + return 0; + } + + /* iphhdr may actually be longer: still need 8 + actual protocol bytes. */ + if (offset + sizeof(icmph) + inner_ip->ihl*4 + 8 + > skb->len) { + limpk("ICMP error internal too short\n"); + return 0; + } + if (!check_ip(skb, offset + sizeof(icmph))) + return 0; + } + } else { + /* CHECK: Can't embed ICMP unless known non-error. */ + if (icmph.type >= sizeof(info)/sizeof(struct icmp_info) + || info[icmph.type].err != ICMP_NOT_ERROR) { + limpk("ICMP type %u not embeddable\n", + icmph.type); + return 0; + } + } + + /* CHECK: Invalid ICMP codes. */ + if (icmph.type < sizeof(info)/sizeof(struct icmp_info) + && (icmph.code < info[icmph.type].min_code + || icmph.code > info[icmph.type].max_code)) { + limpk("ICMP type=%u code=%u\n", + icmph.type, icmph.code); + return 0; + } + + /* CHECK: Above maximum length. */ + if (icmph.type < sizeof(info)/sizeof(struct icmp_info) + && info[icmph.type].max_len != 0 + && skb->len - offset > info[icmph.type].max_len) { + limpk("ICMP type=%u too long: %u bytes\n", + icmph.type, skb->len - offset); + return 0; + } + + switch (icmph.type) { + case ICMP_PARAMETERPROB: { + /* CHECK: Problem param must be within error packet's + * IP header. */ + u_int32_t arg = ntohl(icmph.un.gateway); + + if (icmph.code == 0) { + /* We've already made sure it's long enough. */ + struct iphdr iph; + skb_copy_bits(skb, offset + sizeof(icmph), &iph, + sizeof(iph)); + /* Code 0 means that upper 8 bits is pointer + to problem. */ + if ((arg >> 24) >= iph.ihl*4) { + limpk("ICMP PARAMETERPROB ptr = %u\n", + ntohl(icmph.un.gateway) >> 24); + return 0; + } + arg &= 0x00FFFFFF; + } + + /* CHECK: Rest must be zero. */ + if (arg) { + limpk("ICMP PARAMETERPROB nonzero arg = %u\n", + arg); + return 0; + } + break; + } + + case ICMP_TIME_EXCEEDED: + case ICMP_SOURCE_QUENCH: + /* CHECK: Unused must be zero. */ + if (icmph.un.gateway != 0) { + limpk("ICMP type=%u unused = %u\n", + icmph.type, ntohl(icmph.un.gateway)); + return 0; + } + break; + } + + return 1; +} + +/* UDP-specific checks. */ +static int +check_udp(const struct sk_buff *skb, + unsigned int offset, + unsigned int fragoff, + int more_frags, + int embedded) +{ + struct udphdr udph; + + /* Can't do anything if it's a fragment. */ + if (fragoff) + return 1; + + /* CHECK: Must cover UDP header. */ + if (skb_copy_bits(skb, offset, &udph, sizeof(udph)) < 0) { + limpk("UDP len=%u too short\n", skb->len - offset); + return 0; + } + + /* CHECK: Destination port can't be zero. */ + if (!udph.dest) { + limpk("UDP zero destination port\n"); + return 0; + } + + if (!more_frags) { + if (!embedded) { + /* CHECK: UDP length must match. */ + if (ntohs(udph.len) != skb->len - offset) { + limpk("UDP len too short %u vs %u\n", + ntohs(udph.len), skb->len - offset); + return 0; + } + } else { + /* CHECK: UDP length be >= this truncated pkt. */ + if (ntohs(udph.len) < skb->len - offset) { + limpk("UDP len too long %u vs %u\n", + ntohs(udph.len), skb->len - offset); + return 0; + } + } + } else { + /* CHECK: UDP length must be > this frag's length. */ + if (ntohs(udph.len) <= skb->len - offset) { + limpk("UDP fragment len too short %u vs %u\n", + ntohs(udph.len), skb->len - offset); + return 0; + } + } + + return 1; +} + +/* TCP-specific checks. */ +static int +check_tcp(const struct sk_buff *skb, + unsigned int offset, + unsigned int fragoff, + int more_frags, + int embedded) +{ + struct tcphdr tcph; + unsigned char opt[15 * 4 - sizeof(struct tcphdr)]; + u32 tcpflags; + int end_of_options = 0; + unsigned int i, optlen; + + /* CHECK: Can't have offset=1: used to override TCP syn-checks. */ + /* In fact, this is caught below (offset < 516). */ + + /* Can't do anything if it's a fragment. */ + if (fragoff) + return 1; + + /* CHECK: Smaller than minimal TCP hdr. */ + if (skb_copy_bits(skb, offset, &tcph, sizeof(tcph)) < 0) { + u16 ports[2]; + + if (!embedded) { + limpk("Packet length %u < TCP header.\n", + skb->len - offset); + return 0; + } + + /* Must have ports available (datalen >= 8), from + check_icmp which set embedded = 1 */ + /* CHECK: TCP ports inside ICMP error */ + skb_copy_bits(skb, offset, ports, sizeof(ports)); + if (!ports[0] || !ports[1]) { + limpk("Zero TCP ports %u/%u.\n", + htons(ports[0]), htons(ports[1])); + return 0; + } + return 1; + } + + /* CHECK: TCP header claims tiny size. */ + if (tcph.doff * 4 < sizeof(tcph)) { + limpk("TCP header claims tiny size %u\n", tcph.doff * 4); + return 0; + } + + /* CHECK: Packet smaller than actual TCP hdr. */ + optlen = tcph.doff*4 - sizeof(tcph); + if (skb_copy_bits(skb, offset + sizeof(tcph), opt, optlen) < 0) { + if (!embedded) { + limpk("Packet length %u < actual TCP header.\n", + skb->len - offset); + return 0; + } else + return 1; + } + + /* CHECK: TCP ports non-zero */ + if (!tcph.source || !tcph.dest) { + limpk("Zero TCP ports %u/%u.\n", + htons(tcph.source), htons(tcph.dest)); + return 0; + } + + tcpflags = tcp_flag_word(&tcph); + + /* CHECK: TCP reserved bits zero. */ + if (tcpflags & TCP_RESERVED_BITS) { + limpk("TCP reserved bits not zero\n"); + return 0; + } + + tcpflags &= ~(TCP_DATA_OFFSET | TCP_FLAG_CWR | TCP_FLAG_ECE + | __constant_htonl(0x0000FFFF)); + + /* CHECK: TCP flags. */ + if (tcpflags != TCP_FLAG_SYN + && tcpflags != (TCP_FLAG_SYN|TCP_FLAG_ACK) + && tcpflags != TCP_FLAG_RST + && tcpflags != (TCP_FLAG_RST|TCP_FLAG_ACK) + && tcpflags != (TCP_FLAG_RST|TCP_FLAG_ACK|TCP_FLAG_PSH) + && tcpflags != (TCP_FLAG_FIN|TCP_FLAG_ACK) + && tcpflags != TCP_FLAG_ACK + && tcpflags != (TCP_FLAG_ACK|TCP_FLAG_PSH) + && tcpflags != (TCP_FLAG_ACK|TCP_FLAG_URG) + && tcpflags != (TCP_FLAG_ACK|TCP_FLAG_URG|TCP_FLAG_PSH) + && tcpflags != (TCP_FLAG_FIN|TCP_FLAG_ACK|TCP_FLAG_PSH) + && tcpflags != (TCP_FLAG_FIN|TCP_FLAG_ACK|TCP_FLAG_URG) + && tcpflags != (TCP_FLAG_FIN|TCP_FLAG_ACK|TCP_FLAG_URG + |TCP_FLAG_PSH)) { + limpk("TCP flags bad: 0x%04X\n", ntohl(tcpflags) >> 16); + return 0; + } + + for (i = 0; i < optlen; ) { + switch (opt[i]) { + case 0: + end_of_options = 1; + i++; + break; + case 1: + i++; + break; + default: + /* CHECK: options after EOO. */ + if (end_of_options) { + limpk("TCP option %u after end\n", + opt[i]); + return 0; + } + /* CHECK: options at tail. */ + else if (i+1 >= optlen) { + limpk("TCP option %u at tail\n", + opt[i]); + return 0; + } + /* CHECK: zero-length options. */ + else if (opt[i+1] == 0) { + limpk("TCP option %u 0 len\n", + opt[i]); + return 0; + } + /* CHECK: oversize options. */ + else if (i + opt[i+1] > optlen) { + limpk("TCP option %u at %u too long\n", + (unsigned int) opt[i], i); + return 0; + } + /* Move to next option */ + i += opt[i+1]; + } + } + + return 1; +} + +/* Returns 1 if ok */ +/* Standard IP checks. */ +static int +check_ip(const struct sk_buff *skb, unsigned int offset) +{ + int end_of_options = 0; + unsigned int datalen, optlen; + unsigned int i; + unsigned int fragoff; + struct iphdr iph; + unsigned char opt[15 * 4 - sizeof(struct iphdr)]; + int embedded = offset; + + /* Should only happen for local outgoing raw-socket packets. */ + /* CHECK: length >= ip header. */ + if (skb_copy_bits(skb, offset, &iph, sizeof(iph)) < 0) { + limpk("Packet length %u < IP header.\n", skb->len - offset); + return 0; + } + if (iph.ihl * 4 < sizeof(iph)) { + limpk("IP len %u < minimum IP header.\n", iph.ihl*4); + return 0; + } + + optlen = iph.ihl * 4 - sizeof(iph); + if (skb_copy_bits(skb, offset+sizeof(struct iphdr), opt, optlen)<0) { + limpk("Packet length %u < IP header %u.\n", + skb->len - offset, iph.ihl * 4); + return 0; + } + + fragoff = (ntohs(iph.frag_off) & IP_OFFSET); + datalen = skb->len - (offset + sizeof(struct iphdr) + optlen); + + /* CHECK: Embedded fragment. */ + if (offset && fragoff) { + limpk("Embedded fragment.\n"); + return 0; + } + + for (i = 0; i < optlen; ) { + switch (opt[i]) { + case 0: + end_of_options = 1; + i++; + break; + case 1: + i++; + break; + default: + /* CHECK: options after EOO. */ + if (end_of_options) { + limpk("IP option %u after end\n", + opt[i]); + return 0; + } + /* CHECK: options at tail. */ + else if (i+1 >= optlen) { + limpk("IP option %u at tail\n", + opt[i]); + return 0; + } + /* CHECK: zero-length or one-length options. */ + else if (opt[i+1] < 2) { + limpk("IP option %u %u len\n", + opt[i], opt[i+1]); + return 0; + } + /* CHECK: oversize options. */ + else if (i + opt[i+1] > optlen) { + limpk("IP option %u at %u too long\n", + opt[i], i); + return 0; + } + /* Move to next option */ + i += opt[i+1]; + } + } + + /* Fragment checks. */ + + /* CHECK: More fragments, but doesn't fill 8-byte boundary. */ + if ((ntohs(iph.frag_off) & IP_MF) + && (ntohs(iph.tot_len) % 8) != 0) { + limpk("Truncated fragment %u long.\n", ntohs(iph.tot_len)); + return 0; + } + + /* CHECK: Oversize fragment a-la Ping of Death. */ + if (fragoff * 8 + datalen > 65535) { + limpk("Oversize fragment to %u.\n", fragoff * 8); + return 0; + } + + /* CHECK: DF set and fragoff or MF set. */ + if ((ntohs(iph.frag_off) & IP_DF) + && (fragoff || (ntohs(iph.frag_off) & IP_MF))) { + limpk("DF set and offset=%u, MF=%u.\n", + fragoff, ntohs(iph.frag_off) & IP_MF); + return 0; + } + + /* CHECK: Zero-sized fragments. */ + if ((fragoff || (ntohs(iph.frag_off) & IP_MF)) + && datalen == 0) { + limpk("Zero size fragment offset=%u\n", fragoff); + return 0; + } + + /* Note: we can have even middle fragments smaller than this: + consider a large packet passing through a 600MTU then + 576MTU link: this gives a fragment of 24 data bytes. But + everyone packs fragments largest first, hence a fragment + can't START before 576 - MAX_IP_HEADER_LEN. */ + + /* Used to be min-size 576: I recall Alan Cox saying ax25 goes + down to 128 (576 taken from RFC 791: All hosts must be + prepared to accept datagrams of up to 576 octets). Use 128 + here. */ +#define MIN_LIKELY_MTU 128 + /* CHECK: Min size of first frag = 128. */ + if ((ntohs(iph.frag_off) & IP_MF) + && fragoff == 0 + && ntohs(iph.tot_len) < MIN_LIKELY_MTU) { + limpk("First fragment size %u < %u\n", ntohs(iph.tot_len), + MIN_LIKELY_MTU); + return 0; + } + + /* CHECK: Min offset of frag = 128 - IP hdr len. */ + if (fragoff && fragoff * 8 < MIN_LIKELY_MTU - iph.ihl * 4) { + limpk("Fragment starts at %u < %u\n", fragoff * 8, + MIN_LIKELY_MTU - iph.ihl * 4); + return 0; + } + + /* CHECK: Protocol specification non-zero. */ + if (iph.protocol == 0) { + limpk("Zero protocol\n"); + return 0; + } + + /* FIXME: This is already checked for in "Oversize fragment" + above --RR */ + /* CHECK: Do not use what is unused. + * First bit of fragmentation flags should be unused. + * May be used by OS fingerprinting tools. + * 04 Jun 2002, Maciej Soltysiak, solt@dns.toxicfilms.tv + */ + if (ntohs(iph.frag_off)>>15) { + limpk("IP unused bit set\n"); + return 0; + } + + /* Per-protocol checks. */ + switch (iph.protocol) { + case IPPROTO_ICMP: + return check_icmp(skb, offset + iph.ihl*4, fragoff, + (ntohs(iph.frag_off) & IP_MF), + embedded); + + case IPPROTO_UDP: + return check_udp(skb, offset + iph.ihl*4, fragoff, + (ntohs(iph.frag_off) & IP_MF), + embedded); + + case IPPROTO_TCP: + return check_tcp(skb, offset + iph.ihl*4, fragoff, + (ntohs(iph.frag_off) & IP_MF), + embedded); + default: + /* Ignorance is bliss. */ + return 1; + } +} + +static int +match(const struct sk_buff *skb, + const struct net_device *in, + const struct net_device *out, + const void *matchinfo, + int offset, + unsigned int protoff, + int *hotdrop) +{ + return !check_ip(skb, 0); +} + +/* Called when user tries to insert an entry of this type. */ +static int +checkentry(const char *tablename, + const void *ip, + void *matchinfo, + unsigned int matchsize, + unsigned int hook_mask) +{ + if (matchsize != IPT_ALIGN(0)) + return 0; + + return 1; +} + +static struct ipt_match unclean_match = { + .name = "unclean", + .match = &match, + .checkentry = &checkentry, + .me = THIS_MODULE, +}; + +static int __init init(void) +{ + return ipt_register_match(&unclean_match); +} + +static void __exit fini(void) +{ + ipt_unregister_match(&unclean_match); +} + +module_init(init); +module_exit(fini); +MODULE_LICENSE("GPL");