Tuesday, October 20, 2009

Hack 34 Firewall with OpenBSD's PacketFilter











 < Day Day Up > 





Hack 34 Firewall with OpenBSD's PacketFilter





Use OpenBSD's firewalling

features to protect your network
.





PacketFilter,

commonly known as

PF,

is the firewalling system available in OpenBSD. While it is a

relatively new addition to the operating system, it has already

surpassed IPFilter, the system it has replaced, in both features and

flexibility. PF shares many features with Linux's

Netfilter. Although Linux's Netfilter is more easily

extensible with modules, PF outshines it in its traffic normalization

capabilities and enhanced logging features.





To communicate with the kernel portion of PF, we need to use the

pfctl command. Unlike

the iptables command that is

used with Linux's Netfilter, it is not used to

specify individual rules, but instead uses its own configuration and

rule specification language. To actually configure PF, we must edit

/etc/pf.conf. PF's rule

specification language is actually very powerful, flexible, and easy

to use. The pf.conf file is split up into seven

sections, each of which contains a particular type of rule. Not all

sections need to be used�if you don't need a

specific type of rule, that section can simply be left out of the

file.





The first

section is for macros. In this section you can specify variables to

hold either single values or lists of values for use in later

sections of the configuration file. Like an environment variable or a

programming-language identifier, macros must start with a letter and

also may contain digits and underscores.





Here are some example macros:





EXT_IF="de0"

INT_IF="de1"

RFC1918="{ 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 }"




A macro can be referenced later by prefixing it with the

$ character:





block drop quick on $EXT_IF from any to $RFC1918




The second section allows you to specify tables

of IP addresses to use in later rules. Using tables for lists of IP

addresses is much faster than using a macro, especially for large

numbers of IP addresses, because when a macro is used in a rule, it

will expand to multiple rules, with each one matching on a single

value contained in the macro. Using a table adds just a single rule

when it is expanded.





Rather than using the macro from our previous example, we can define

a table to hold the nonroutable RFC 1918 IP addresses:





table <rfc1918> const { 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 }




The const keyword ensures that this table cannot

be modified once it has been created. Tables are specified in a rule

in the same way that they were created:





block drop quick on $EXT_IF from any to <rfc1918>




You can also load a list of IP addresses into a table by using the

file keyword:





table <spammers> file "/etc/spammers.table"




If you elect not to use the const keyword, then

you can add addresses to a table by running a command such as this:





pfctl -t spammers -T add 10.1.1.1




Additionally, you can delete an address by running a command like

this:





pfctl -t spammers -T delete 10.1.1.1




To list the contents of a table, you can run:





pfctl -t spammers -T show




In addition to IP addresses, hostnames may also be specified. In this

case, all valid addresses returned by the resolver will be inserted

into the table.





The next section of the configuration file contains

options

that affect the behavior of PF. By modifying options, we can control

session timeouts, defragmentation timeouts, state-table transitions,

statistic collection, and other behaviors. Options are specified by

using the set keyword. The number of options is

too numerous to discuss all of them in any meaningful detail;

however, we will discuss the most pertinent and useful ones.





One of the most important options is

block-policy. This option specifies

the default behavior of the block keyword and can

be configured to silently drop matching packets by specifying

drop. Alternatively,

return may be used, to specify that

packets matching a block rule will generate a TCP reset or an ICMP

unreachable packet, depending on whether the triggering packet is TCP

or UDP. This is similar to the REJECT target in

Linux's Netfilter.





For example, to have PF drop packets silently by default, add a line

like this to /etc/pf.conf:





set block-policy drop




In addition to setting the block-policy,

additional statistics such as packet and byte counts can be collected

for an interface. To enable this for an interface, add a line similar

to this to the configuration file:





set loginterface de0




However, these statistics can only be collected on a single interface

at a time. If you do not want to collect any statistics, you can

replace the interface name with the none keyword.





To better utilize resources on busy networks, we can also modify the

session-timeout values. Setting

this to a low value can help improve the performance of the firewall

on high-traffic networks, but at the expense of dropping valid idle

connections.





To set the session timeout (in seconds), put a line similar to this

in /etc/pf.conf:





set timeout interval 20




With this setting in place, any TCP connection that is idle for 20

seconds will automatically be reset.





PF can also optimize performance on low-end hardware by tuning its

memory use regarding how many states may be stored at any one time or

how many fragments may reside in memory for fragment reassembly. For

example, to set the number of states to 20,000 and the number of

entries used by the fragment reassembler to 15,000, we could put this

in our pf.conf:





set limit states 20000

set limit frags 15000




Alternatively, we could combine these entries into a single one, like

this:





set limit { states 20000, frags 15000 }




Moving on, the next section is for traffic

normalization rules. Rules of this type ensure that packets passing

through the firewall meet certain criteria regarding fragmentation,

IP IDs, minimum TTLs, and other attributes of a TCP datagram. Rules

in this section are all prefixed by the scrub

keyword. In general, just putting scrub all is

fine. However, if necessary, we can get quite detailed in specifying

what we want normalized and how we want to normalize it. Since we can

use PF's general filtering-rule syntax to determine

what types of packets a scrub rule will match, we can normalize

packets with a great deal of control.





One of the more interesting possibilities is to randomize all

IP IDs in the packets leaving your

network for the outside world. In doing this, we can make sure that

passive operating system determination methods based on IP IDs will

break when trying to figure out the operating system of a system

protected by the firewall. Because such methods depend on analyzing

how the host operating system increments the IP IDs in its outgoing

packets, and our firewall ensures that the IP IDs in all the packets

leaving our network are totally random, it's pretty

hard to match them against a known pattern for an operating system.

This also helps to prevent enumeration of machines in a

network

address translated (NAT) environment. Without random IP IDs, someone

outside the network can perform a statistical analysis of the IP IDs

being emitted by the NAT gateway in order to count the number of

machines on the private network. Randomizing the IP IDs defeats this

kind of attack.





To enable random ID generation on an interface, put a line such as

this in /etc/pf.conf:





scrub out on de0 all random-id




We can also use the scrub

directive to reassemble

fragmented packets before forwarding them to their destinations. This

helps prevent specially fragmented packets (such as packets that

overlap) from evading intrusion-detection systems that are sitting

behind the firewall.





To enable fragment reassembly on all interfaces, simply put the

following line in the configuration file:





scrub fragment reassemble




If we want to limit reassembly to just a single interface, we can

change this to:





scrub in on de0 all fragment reassemble




This will enable fragment reassembly for the de0

interface.





The next two sections of the pf.conf file

involve packet queuing and address translation, but since this hack

focuses on packet filtering,

we'll skip these. This brings us to the

last section, which contains the actual

packet-filtering rules. In general, the

syntax for a filter rule can be defined by the following:





action direction [log] [quick] on int [af] [proto protocol] \

from src_addr [port src_port] to dst_addr [port dst_port] \

[tcp_flags] [state]




In PF, a rule can have only two actions: block and pass. As discussed

previously, the block policy affects the behavior of the

block action. However, this can be modified for specific rules by

specifying it along with an action, such as block

drop
or block return. Additionally,

block return-icmp can be used, which will return

an ICMP unreachable message by default. An ICMP type can be specified

as well, in which case that type of ICMP message will be returned.





For most purposes, we want to start out with a default

deny policy; that way we can

later add rules to allow the specific traffic that we want through

the firewall.





To set up a default deny policy for all interfaces, put the following

line in /etc/pf.conf:





block all




Now we can add rules to allow traffic through our firewall. First

we'll keep the loopback interface unfiltered.

To accomplish this, we'll use this rule:





pass quick on lo0 all




Notice the use of the quick keyword. Normally PF

will continue through our rule list even if a rule has already

allowed a packet to pass, in order to see whether a more specific

rule that appears later on in the configuration file will drop the

packet. The use of the quick keyword modifies this

behavior and causes PF to stop processing the packet at this rule if

it matches the packet and to take the specified action. With careful

use, this can greatly improve the performance of a ruleset.





To prevent external hosts from spoofing internal addresses, we

can use the antispoof keyword:





antispoof quick for $INT_IF inet




Next we'll want to block any packets from entering

or leaving our external interface that have a nonroutable RFC 1918 IP

address. Such packets, unless explicitly allowed later, would be

caught by our default deny policy. However, if we use a rule to

specifically match these packets and use the quick

keyword, we can increase performance by adding a rule like this:





block drop quick on $EXT_IF from any to <rfc1918>




If we wanted to allow traffic into our network destined for a web

server at 192.168.1.20, we could use a rule like this:





pass in on $EXT_IF proto tcp from any to 192.168.1.20 port 80 \

modulate state flags S/SA




This will allow packets destined to TCP port 80 at 192.168.1.20 only

if they are establishing a new connection (i.e., the

SYN flag is set), and will enter the connection

into the state table. The modulate keyword ensures

that a high-quality initial sequence number is generated for

the session, which is important if the operating system in use at

either end of the connection uses a poor algorithm for generating its

ISNs.





Similarly, if we wanted to pass traffic to and from an email server

at the IP address 192.168.1.21, we could use this rule:





pass in on $EXT_IF proto tcp from any to 192.168.1.21 \

port { smtp, pop3, imap2, imaps } modulate state flags S/SA




Notice that multiple ports can be specified for a rule by separating

them with commas and enclosing them in curly braces. We can also use

service names, as defined in /etc/services,

instead of specifying the service's port number.





To allow traffic to a DNS server at 192.168.1.18, we can add a rule like this:





pass in on $EXT_IF proto tcp from any to 192.168.1.18 port 53 \

modulate state flags S/SA




This still leaves the firewall blocking UDP DNS traffic. To allow this

through, add this rule:





pass in on $EXT_IF proto udp from any to 192.168.1.18 port 53 \

keep state




Notice here that even though this is a rule for UDP packets we have

still used the state keyword. In this case, PF

will keep track of the connection using the source and destination IP

address and port pairs. Also, since UDP datagrams do not contain

sequence numbers, the modulate keyword is not

applicable. We use keep state instead, which is

how to specify stateful inspection when not modulating ISNs. In

addition, since UDP datagrams do not contain flags, we simply omit

them.





Now we'll want to allow connections initiated from

the internal network to pass through the firewall. To do this,

we'll need to add the following rules to let the

traffic into the internal interface of the firewall:





pass in on $INT_IF from $INT_IF:network to any

pass out on $INT_IF from any to $INT_IF:network

pass out on $EXT_IF proto tcp all modulate state flags S/SA

pass out on $EXT_IF proto { icmp, udp } all keep state




As you can see, OpenBSD has a very powerful and flexible firewalling

system. There are too many features and possibilities to discuss

here. For more information, you can look at the excellent PF

documentation available online or the pf.conf

manpage.

















     < Day Day Up > 



    No comments: