by Israel G. Lugo, Don Parker
The concept of a firewall still brings to mind the picture of an impenetrable brick wall, the unsurpassable magic protector of all that is good. The bold statements made by today's security vendors only emphasize this, with claims of complete and automatic security, with a wall able to block all perils dead in their tracks using logic that perhaps didn't exist two years ago. But what if in reality the wall of the firewall is made of straw?
To answer this question, we need to go over a few basic concepts. First of all, what is a software firewall? At its core, it is nothing more than a filter -- one that sits between your normal applications and the networking components of the operating system and decides what it will let through and what it will not. For this, it implants itself in key places of the application/network path, and analyzes what goes through against a rule set, according to specific sets of criteria. Anything that falls under the "allow" rules is allowed to pass on, and conversely anything that does not is silently dropped (with an optional log message, on-screen alert, and so on).
What does this mean from a design point of view? What are the criteria that the information is analyzed against? Well, there are two main areas that a firewall must focus on, resulting in two main components in the design of a software firewall.
The first is at the actual packet level (layers 3 and 4 of the OSI model). The job here is to look for suspicious or malformed packets, detect port scans, and assess whether or not each packet should be allowed into the protocol stack. Packets will be analyzed by networking criteria, for example: formal validity of the packet; direction of the packet (inbound or outbound); source host and port; destination host and port; specific packet flags (is this a connection attempt? Does it belong to an already established connection?).
The other component of the firewall works at a higher level, dealing with individual processes -- checking whether process X should be allowed to initiate a connection to a given host on a given port, whether it should be allowed to listen for connections on a given range of ports, and so on. Basically, the firewall will act both as a per-packet and a per-process filter.
How is this done? Exact details vary from product to product of course, but functionally, at the core, is mostly the same. A true software firewall (that is, excluding the rather useless ones that merely hook socket calls) uses two drivers for the actual filtering: one for the packet filtering, another for the per-process filtering. Then there is the front end, the graphical interface, where the user changes settings. The important work is done inside the kernel, using just two drivers.
The packet filter
Concerning the lower level, packet filtering driver, this is typically implemented in one of two ways.
One method involves the use of a NDIS (Network Driver Interface Specification) intermediate driver. Basically, this driver will sit between the Network Interface Card's driver and the protocol drivers (TCP/IP, etc). It will, in essence, become a virtual adapter, appearing to the protocol drivers as if it were the NIC driver, and vice-versa. As each packet inbound or outbound will go through the intermediate driver, this will permit it to stage a "man-in-the-middle" attack (obviously in a benign way in this case).
The other common method of implementing a packet filter is with tapping into the NDIS itself, hooking a subset of the NDIS library functions used by the protocol drivers. This means the packet filter driver will tap into the NDIS wrapper itself, sitting right between the protocol drivers and anything below. Although this is a totally different implementation, it is functionally similar to the above method.
Now, the job of the packet filter driver seems simple enough. It will analyze every packet it sees, according to the criteria specified in the firewall rules stored in some internal data structure. It will look for things such as source and target hosts and ports, level of fragmentation, protocol type, packet flags, whether or not the packet is part of an already open connection, and so on. For example, if the protocol is TCP and the packet has the SYN flag set (an attempt to open a connection), the filter will look up in its rules whether or not to allow opening the connection, based on the source and target hosts and ports. If the connection is allowed, the filter will add it to an internal list of open connections. This is how the firewall keeps track of open connections, forming the base of stateful packet inspection. If a packet is allowed by the rules, or if it belongs to a connection on the list of open connections, it will be allowed through. The packet filter driver will pass it along to the next layer - either the protocol drivers if the packet was inbound, or the miniport/intermediate drivers if it was outbound. If the packet is blocked by the rules, it will be silently dropped. It will not be passed on to the next layer, which will therefore have no knowledge of it ever existing. Optionally, a waiting thread can also be signalled to show some feedback such as an alert on screen, or write to a log file.
The per-process filter
There is still the other driver to consider in a software firewall, and for good reason. At the levels we're dealing with in the above filtering (NDIS intermediate or NDIS library wrapper), there is no process information. Everything is just packets that are going in or out, things are happening conceptually from outside the machine from an application point of view. As such, to do the actual per-process filtering, another filtering level is necessary, sitting at a higher place in the chain. Generally, this will be done from kernel space, by wrapping the TDI (Transport Driver Interface) functionality, intercepting the functions that the applications and/or helper DLLs (WinSock) use to communicate data to and from the transport protocol drivers.
There are other methods as well. For example, the WinSock API, the set of functions the vast majority of applications use to access the network, is built upon a layered model that allows for third party extensions to be inserted between the application interface and the base network protocol. Such an extension can be added by implementing a Layered Service Provider (LSP), and inserting it into the LSP chain. An LSP is a standard Windows DLL that obeys certain design specifications, and has the specific function of being inserted in the WinSock protocol chain. In the WinSock model, all networking data goes through this chain, with each LSP being responsible for passing it along to the LSP immediately above (or below, depending on whether the data is incoming or outgoing), after having optionally processed or altered the data according to its own function. Quality of Service (QoS) is an example of one such extension, implemented as a LSP. The firewall's per-process filter could be implemented as an LSP, sitting inside the protocol chain and selectively passing data on to the rest of the chain or silently dropping it according to its own criteria.
The LSP method, however, is not the most thorough filtering solution, as it relies on applications using WinSock to communicate. To bypass the LSP per-process filter, a rogue application would just need to use a driver of its own to communicate directly with the protocol driver through the TDI, thereby bypassing WinSock. The first described method, wrapping the upper edge of the TDI itself, is a better alternative, as it works at a lower level.
The job of the per-process filter is to analyze communication attempts by applications. It will look at the process ID (PID) of the process attempting to send or receive data, and analyze its characteristics against the rule set, under a more or less complicated set of criteria, depending on the thoroughness of the firewall's per-process filtering implementation. The crucial question, of course, is to ask, "is the executable that generated this process allowed to perform the action it's trying to perform?". This is a matter of finding out the executable file from the PID, optionally performing a hash to verify the file's integrity against a known hash, looking for the file in the rule set, then checking if it is allowed to perform that action or not.
Problems with per-process filtering
There are concerns with this implementation, however. For example, it would be desirable to check whether the process has any modules (DLLs) attached, and if so whether these modules are supposed to be there or not. This makes sense since the code in attached modules will execute from within the context of the process they are attached to, so if an illegitimate process attaches EVIL.DLL to good process X, good process X will in effect be running the code from EVIL.DLL along its own. The solution for this requires enumerating the loaded modules, finding their image files (path and filename of the attached DLLs), then checking against a table of modules allowed to attach to that process. Firewalls often check for this and ask whether to allow the module; but obviously this is only a placebo in a sense. The firewall can't possibly know all possible present and future DLLs, good and bad, that might exist, and therefore it is up to the user to guess whether a given DLL is good or not. Yet another concern is whether the process has had its code altered in memory (a process can alter another process' memory region, changing it's code on the fly to alter its behaviour). Or if the process was launched by a malicious program (which would also allow the malicious program to alter the process' behaviour). And the list goes on.
Only a few of these problems are adequately addressed in today's firewalls. The truth is there are simply too many ways for malicious code to disguise itself in order to fool a software firewall's process filtering, ranging from the simple to the very complex. An illegitimate process could patch another legitimate process in memory. It could take advantage of a legitimate program's behaviour, for example by launching a browser with a command line telling it to load a given malicious page. Even in the advent of the process filter scanning for parent-child relationships, multiple levels of indirection could be used (the illegal process could launch cmd.exe with a command line telling it to launch another cmd.exe, which will then launch the browser with the intended page). Or the rogue application could adopt aggressive tactics against the firewall, disabling it altogether, faking mouse clicks to click through the alerts... Remember, anything the user can do, a program can do for him (within this context, obviously).
And all of the above is just focusing on fooling the per-process filter. A whole other world of opportunities opens up when the attacker realizes he doesn't even need to go through the process filter at all. Remember, below the process filter, information about processes is lost. Information just becomes data coming in or out of the machine, and that is as it appears to the other filter, the packet filter. All the attacker has to do is use a driver to inject the packets below the level where the process filtering is being done, and then he only has to worry about the packet filter. As long as he uses an already allowed port (say port 80, 25, or whatever), the packet filter will happily allow it. Not even to mention the possibility of just going under the packet filter...
In this article we've looked at how a software firewall actually works, by implementing both a packet filter and a per-process filter. We also looked at the problems with this approach, and how the per-process filter in particular can be tricked into allowing malicious code access to the network anyway. Next time, in the second and final part of this article, we look at ways of circumventing a software firewall's modus operandi altogether, effectively making the firewall look as if it were made of "straw."
This article originally appeared on SecurityFocus.com -- reproduction in whole or in part is not allowed without expressed written consent.