Building.Open.Source.Network.Security.Tools.Components.And.Techniques [Electronic resources] نسخه متنی

This is a Digital Library

With over 100,000 free electronic resource in Persian, Arabic and English

Building.Open.Source.Network.Security.Tools.Components.And.Techniques [Electronic resources] - نسخه متنی

Mike D. Schiffman

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید







Firewalxsxsk Code Walkthrough

The following section contains a detailed code walkthrough of the Firewalk 5.0 source tree. At this writing, the Firewalk codebase consisted of approximately 2200 lines of code spread across 14 source files (which you can find at the end of this chapter). The walkthrough contains selected code that shows control and flow exactly as it appears from the source files.





Note

Most functions return 1 on success and -l on failure. This style is the author's, in line with libnet and libsf.


Intermingled with the code walkthrough are flowcharts that show the high-level processes that are taking place in the code. The Firewalk walkthrough is broken down into three main areas of focus: initialization and the two packet sending phases and ramping and scanning. This high-level process is shown in the top-level flowchart in Figure 12.2.


Figure 12.2: Firewalk top-level flowchart.

The Firewalk network security tool performs its work in three stages. First, the tool goes through a bootstrap process where it initializes itself and validates user command-line arguments. Assuming that the initialization process is successful, Firewalk moves on to the ramping phase, where it determines the proper hopcount to the binding host beyond the target gateway. If the ramping phase completes successfully, Firewalk then attempts to determine ACL status on the target gateway during the scanning phase.


Stage One: Initialization


Firewalk initialization is a three-step process, as shown in Figure 12.3.


Figure 12.3: Firewalk initialization flowchart.

The first thing Firewalk does after greeting the user is allocate memory for its monolithic context structure, which—as the following comment states-is used by every major function in the program. The information contained in this structure is central to the operation of the program:


int
main(int argc, char *argv[])
{
int c;
struct firepack *fp;
char *port_list = NULL;
char errbuf[FW_ERRBUF_SIZE];
printf("Firewalk 5.0 [gateway ACL scanner]\n");
/*
* Initialize the main control context. We keep all of our program
* state here and this is used by just about every function in the
* program.
*/
if (fw_init_context(&fp, errbuf) == -1)
{
fprintf(stderr, "fw_init_control(): %s n", errbuf);
goto done;
}

The control context, prior to initialization, is shown in Figure 12.4.


Figure 12.4: Firewalk context.

The Firewalk context is a 364-byte monolithic structure containing all of the information necessary to describe a Firewalk session. The structure was designed to align cleanly on 8-byte boundaries so no internal padding will result. A structure handle is passed to nearly every function in the program to allow them to access and modify fields as needed.

The fw_init_context() function allocates memory for the control context and sets some of the default values. After processing the command-line arguments supplied by the user (not shown), Firewalk then initializes the network components by using fw_init_net(). The function requires the address of the control context pointer fp, a very common theme for most Fire-walk functions, the user-supplied canonical target gateway and metric IP addresses, and a pointer to the port list to be scanned (which will be NULL if the user did not specify one):


/* initialize the network components */
if (fw_init_net(&fp, argvfoptind], argv[optind + 1], port_list) == -1)
{
fprintf(stderr, "fw_init_network(): %s n", fp->errbuf);
goto done;
}

The following function, fw_init_net(), performs the lion's share of the initialization. The function brings up all of the networking components, sets the rest of the context defaults, and constructs the initial packet template:


int
fw_init_net(struct firepack **fp, char *gw, char *m, char *port_list)
{
#if HAVE_BPF
int one;
#endif
char errbuf[PCAP_ERRBUF_SIZE];

Before any packets can be injected into the network, Firewalk needs a libnet context to call many of the libnet functions:


/* get a libnet context */
(*fp)->l = libnet_init(LIBNET_LINK, (*fp)->device, errbuf);
if ((*fp)->1 == NULL)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE, "libnet_init(): %s",
errbuf);
return (-1);
}

If the user did not specify a device at the command line, Firewalk is forced to look for one. Fortunately, libnet makes this process painless:


/* get our device if the user didn't specify one*/
if ((*fp)->device == NULL)
{
(*fp)->device = libnet_getdevice((*fp)->1);
}

Firewalk needs to know the source address of the interface for which it will send and receive packets:


/* get the source address of our outgoing interface */
(*fp)->sin.sin_addr.s_addr = Iibnet_get_ipaddr4((* fp)->1);

Next, it verifies that the target gateway and metric addresses are valid IPv4 addresses and that they are not the same:


/* setup the target gateway */
if (((*fp)->gateway = Iibnet_name2addr4((*fp)->1, gw, 1)) == -1)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE,
"Iibnet_name2addr4(): %s (target gateway: %s)",
libnet_geterror((*fp)->l), gw);
return (-1);
}
/* setup the metric */
if (((*fp)->metric = Iibnet_name2addr4((*fp)->1, m, 1) ) == -1)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE,
"libnet_nanie2addr4() : %s (metric: %s)",
libnet_geterror((*fp)->1), m);
return (-1);
}
/* sanity check */
if ((*fp)->gateway == (*fp)->metric)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE,
"target gateway and metric cannot be the same");
return (-1);
}

If the user specified a list of ports to scan during Firewalk's operation, this list will be utilized. Otherwise, if no list was specified, it will use the default port list, which is ports 1-130,139, and 1025.


/* get our port list stuff situated */
if (libnet_plist_chain_new((*fp)->1, &(*fp)->plist,
port_list == NULL ? strdup(FW_DEFAULT_PORT_LIST) :
port_list) == -1)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE,
"libnet_plist_chain_new(): %s\n", libnet_geterror((*fp)->1));
return (-1);
}

Next, Firewalk initializes a libpcap context (required for packet sniffing and filtering):


/* get a libpcap context */
(*fp)->p = pcap_open_live((*fp)->device, FW_SNAPLEN, 0, 0, errbuf);
if ( ( (*fp)->p) == NULL)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE, "pcap_open_live(): %s",
errbuf);
return (-1);
}

By default, BPF (which is what libpcap uses for low-level packet capture on BSD-derived systems) is initialized with immediate mode (BIOCIMMEDIATE) turned off. When this feature is deactivated, BPF will wait to return from a read until the kernel buffer fills up with packets or until a timeout occurs. This process is generally more efficient because it means that the program will interrupt the kernel less frequently, but it is a poor performer for real-time applications such as Firewalk (this is also the same behavior that we saw in Lilt in Chapter 4). In order to obviate this situation, Firewalk makes an ioctl() call on the BPF device to enable immediate mode, requiring BPF to return immediately on the first packet read from the network, thus increasing performance:


#if HAVE_BPF
/*
* BPF, by default, will buffer packets inside the kernel until
* either the timer expires (which we do not use) or when the
* buffer fills up. This is not sufficient for us since we could
* miss responses to our probes. So we set BIOCIMMEDIATE to tell
* BPF to return immediately when it gets a packet. This is pretty
* much the same behavior we see with Linux which returns every
* time it sees a packet. This is less than efficient since we're
* spending more time interrupting the kernel, but hey, we gotta
* get our work done!
*/
one = 1;
if (ioctl(pcap_fileno((*fp)->p), BIOCIMMEDIATE, &one) < 0)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE,
"ioctl(): BIOCIMMEDIATE: %s", strerror(errno));
return (-1);
}
#endif

While it is always a good practice to determine the size of the link layer header, Firewalk has not been extensively tested on anything other than Ethernet. In any event, it goes on to determine the size of some more popular link layer headers:



/* get the datalink size */
switch (pcap_datalink((*fp)->p))
{
case DLT_SLIP:
(*fp)->packet_offset = 0x10;
break;
case DLT_RAW:
(*fp)->packet_offset = 0x00;
break;
case DLT_PPP:
(*fp)->packet_offset = 0x04;
break;
case DLT_EN10MB:
default:
(*fp)->packet_offset = 0x0e;
break;
}

To make the packet-capturing operation more efficient, Firewalk sets a libp-cap filter to ignore all but a few types of traffic (the specifics of which depend on the scanning protocol). For UDP-based scans, Firewalk is only interested in ICMP unreachable and ICMP TTL expired messages. For TCP-based scans, Firewalk needs to read only the ICMP messages previously listed, along with the TCP SYN and TCP RST packets:


/*
* Set pcap filter and determine outgoing packet size. The filter
* will be determined by the scanning protocol:
* UDP scan:
* icmp[0] == 11 or icmp[0] == 3 or udp
* TCP scan:
* icmp[0] == 11 or icmp[0] == 3 or tcp[14] == 0x12 or tcp[14] * == 0x4 or tcp[14] == 0x14
*/
switch ((*fp)->protocol)
{
case IPPROTO_UDP:
if (fw_set_pcap_filter(FW_BPF_FILTER_UDP, fp) == -I)
{
/* err msg set in fw_set_pcap_filter() */
return (-1);
}
/* IP + UDP */
(*fp)->packet_size = LIBNET_IPV4_H + LIBNET_UDP_H;
break;
case IPPROTO_TCP:
if (fw_set_pcap_filter(FW_BPF_FILTER_TCP, fp) == -1)
{
/* err msg set in fw_set_pcap_filter() */
return (-1);
}
/* IP + TCP */
(*fp)->packet_size = LIBNET_IPV4_H + LIBNET_TCP_H;
/* randomize the TCP sequence number */
libnet_seed_prand((*fp)->1);
(*fp)->seq = libnet_get_prand(LIBNET_PRu32);
break;
default:
sprintf((*fp)->errbuf,
"fw_init_network(): unsupported protocol");
return (-1);
}

In the last stage of initialization, Firewalk builds the packet template used in later phases of operation. The fw_packet_build_probe() function, as shown here, builds an initial packet template that updates as the packet sending phases progress:


/*
* Build a probe packet template. We'll use this packet template
* over and over for each write to the network, modifying certain
* fields (IP TTL, UDP/TCP ports and of course checksums as we go).
*/
if (fw_packet_build_probe(fp) == -1)
{
/* error msg set in fw_packet_build_probe() */
return (-1);
}
return (1);
}

The full mechanics of the packet template construction begin as the code moves from fw_init_net() to fw_packet_build_probe() :


int
fw_packet_build_probe(struct firepack **fp)
{
arp_t *a;
route_t *r;
struct arp_entry arp;
struct route_entry route;

As seen in Chapter 3, Libnet mimics the OS kernel when building packet headers by beginning with the highest layer and progressing downward to the MAC layer. Firewalk starts by building the transport layer header (these functions are shown later on):


/* first build our transport layer header */
switch ((*fp)->protocol)
{
case IPPROTO_UDP:
if (fw_packet_build_udp(fp) == -1)
{
/* error msg set in fw_packet_build_udp() */
return (-1);
}
break;
case IPPROTO_TCP:
if (fw_packet_build_tcp(fp) == -1)
{
/* error msg set in fw_packet_build_tcp() */
return (-1);
}
break;
default:
sprintf((*fp)->errbuf,
"fw_packet_build_probe(): unknown protocol");
return (-1);
}

After the transport layer header is built, Firewalk moves on to the IPv4 header. Note that the libnet ptag is saved to the context structure. Firewalk will need to refer back to this header during later phases:


/* build our IPv4 header */
(*fp)->ip = Iibnet_build__ipv4 (
(*fp)->packet_size, /* packetlength */
0, /* IP tos */
(*fp)->id, /* IP id */
0, /* IP frag bits */
(*fp)->ttl, /* IP time to live */
(*fp)->protocol, /* transport protocol */
0, /* checksum */
(*fp)->sin.sin_addr.s_addr, /* IP source */
(*fp)->metric, /* IP destination */
NULL, /* IP payload */
0, /* IP payload size */
(*fp)->l, /* libnet context */
0); /* No saved ptag */
if ((*fp)->ip == -1)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE, "Iibnet_build_ipv4() %s",
libnet_geterror((*fp)->1));
return (-1);
}

For maximum flexibility, control, and portability, Firewalk uses libnet's link layer interface to send packets. In order to send packets to arbitrary Internet hosts, however, Firewalk needs to determine the MAC address of the first hop gateway. This task is accomplished with libdnet's portable ARP cache and route table manipulation functionality:


/*
* Now we need to get the MAC address of our first hop gateway.
* Dnet to the rescue! We start by doing a route table lookup
* to determine the IP address we use to get to the
* destination host (the metric).
*/

To look up the MAC address of the default gateway, Firewalk first must determine what the IP address of the default gateway actually is. This task is accomplished by performing a lookup against the local routing table:


r = route_open();
if (r == NULL)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE, "route_open()");
route_close(r);
return (-1)
}
/* convert the metric address to dnet's native addr_t format */
if (addr_aton(libnet_addr2name4((*fp)->metric, 0),
&route.route_dst) 0)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE, "addr_aton()");
route_close(r);
return (-1);
}
/* get the route entry telling us how to reach the metric */
if (route_get(r, &route) < 0)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE, "route_get()");
route_close(r);
return (-1);
}
route_close(r);

Armed with the gateway's IP address, Firewalk then performs a simple ARP table lookup to find out the MAC address of the first hop gateway:


a = arp_open();
if (a == NULL)
{
snprintf((*fpj->errbuf, FW_ERRBUF_SIZE, "arp_open()");
return (-1);
}
/* get the MAC of the first hop gateway */
arp.arp_pa = route.route_gw;
if (arp_get(a, &arp) < 0)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE, "route_get()");
arp_close(a);
return (-1);
}
arp_close(a);

With the MAC address of the first hop known, Firewalk can now build the Ethernet header:


/* build our ethernet header */
if (libnet_autobuild_ethernet(
(u_char *)&arp.arp_ha.addr_eth,
ETHERTYPE_IP,
(*fp)->l) == -1)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE,
"libnet_autobuild_ethernet() %s",
libnet_geterror((*fp)->1));
arp_close(a);
return (-1);
}
return (1);
}

The following two functions build the UDP and TCP headers used in the ramping phase (the reader should again note how the ptags are saved for later use):


int
fw_packet_build_udp(struct firepack **fp)
{
/* build a UDP header */
(*fp)->udp = libnet_build_udp(
(*fp)->sport, /* source UDP port */
(*fp)->dport, /* dest UDP port */
(*fp)->packet_size - LIBNET_IPV4_H, /* UDP size */
0, /* checksum */
NULL, /* IP payload */
0, /* IP payload size */
(*fp)->l, /* libnet context */
0); /* No saved ptag */
if ((*fp)->udp == -I)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE, "libnet_build_udp(} %s",
libnet_geterror((*fp)->1));
return (-1);
}
return (1);
}
int
fw_packet_build_tcp(struct firepack **fp)
{
/* build a TCP header */
(*fp)->tcp = libnet_build_tcp(
(*fp)->sport, /* source TCP port */
(*fp)->dport, /* dest TCP port */
(*fp)->seq, /* sequence number */
OL, /* ACK number */
TH_SYN, /* control flags */
1024, /* window size */
0, /* checksum */
0, /* urgent */
(*fp)->packet_size - LIBNET_IPV4_H, /* TCP size */
NULL, /* IP payload */
0, /* IP payload size */
(*fp)->l, /* libnet context */
0); /* No saved ptag */
if ((*fp)->tcp == -1)
{
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE, "libnet_build_tcp() %s",
libnet_geterror((*fp)->l)); return (-1);
}
return (1);
}

The initialization phase is considered complete after the construction of the packet template. The Firewalk context, after an initialization based upon the user supplying a -n switch, a target gateway, and a metric to the command line, appears in Figure 12.5.


Figure 12.5: Firewalk context after initialization.

At this point in the application's execution, the collection of pointers in the Firewalk context are all initialized; device points to the canonical name for the interface, packet points to NULL (it will soon point to the captured packet), p and 1 point to the libpcap and libnet contexts, respectively, and plist points to the libnet port list. It is also important to note the libnet ptags that refer to the packet headers inside the libnet context.


Stage Two: Ramping Phase


After a successful initialization, the ramping phase commences, which is shown in Figure 12.6.


Figure 12.6: Firewalk ramping phase flowchart.

The ramping phase is a multi-staged process that revolves around a packet injection, capturing, and processing loop. Most of the real work occurs inside fw_packet_capture(), which is saved until the end of the ramping section.

Returning to the main module, Firewalk calls the main scanning driver:


/* execute scan: phase one, and hopefully phase two */
switch (firewalk(&fp))
{

Firewalk will only hit this branch if there is a serious, unrecoverable error:


case -1:
case FW_SERIOUS_ERROR:
/* grievous error of some sort */
fprintf (stderr, "firewalkO: %s\n", fp->errbuf);
break;

If Firewalk exceeds its hopcount or the metric is reached before the target gateway, the following error will be thrown:


case FW_ABORT_SCAN:
/* hop count exceeded or metric en route */
fprintf(stderr, "Scan aborted: %s. n", fp->errbuf);
break;

This event occurs if the user hits Ctrl-c on the keyboard:


case FW_USER_INTERRUPT:
fprintf(stderr, "Scan aborted by user. n");
break;

Finally, if everything goes according to plan,


default:
printf("Scan completed successfully. n");
break;
}

The operation of the scanning driver can be broken down into two parts corresponding to each phase. The first part covers the ramping phase:


int
firewalk(struct firepack **fp)
{
int done, i, j;
u_short bport, cport, eport;

Firewalk begins by informing the user as to the content of the parameter collection utilized in the scan:


/* inform the user what's what */
printf("%s-based scan.Xn",
(*fp)->protocol == IPPROTO_TCP ? "TCP" : "UDP");
printf("Ramping phase source port: %d, destination port: %d n",
(*fp)->sport, (*fp)->dport);
if ((*fp)->flags & FW_STRICT_RFC && (*fp)->protocol == IPPROTO_TCP)
{
printf("Using strict RFC adherence. n");
}
printf("Hotfoot through %s using %s as a metric./n",
libnet_addr2name4(((*fp)->gateway),
((*fp)->flags) & FW_RESOLVE),
libnet_addr2name4(((*fp)->metric),
((*fp)->flags) & FW_RESOLVE));
/*
* PHASE ONE: Firewalk hopcount ramping
* A standard Traceroute-style IP expiry scan is initiated towards
* the metric, with the intent being to find how many hops away the
* target gateway is from the scanning host. We'll increment the
* hopcounter and update packet template each pass through the loop.
*/
printf("Ramping Phase: n");

The main ramping loop starts here and continues until either Firewalk reaches its hopcount or the scan is "done" (the sentinel variable done is set to 1). The scan can be considered "done" when either the binding host is reached or Firewalk gets a terminal packet from the metric:


for (done = 0, i = 0; !done && i < FW_IP_HOP_MAX; i++)
{
/* send a series of probes (currently only one) */
for (j = 0; j < 1; j++)
{
fprintf(stderr, "%2d (TTL %2d): ", i + 1, (*fp)->ttl);

The ramping probes are injected at this point to the network. fw_packet_ inject() (not shown) is a simple wrapper lo libnec_write().


if (fw_packet_inject(fp) == -1)
{
/*
* Perhaps this write error was transient. We'll hope
* for the best. Inform the user and continue.
*/
fprintf(stderr, "fw_packet_inject(): %s n",
(*fp)->errbuf);
continue;
}

As we stated before, the bulk of the analysis work is done in fw_packet_ capture(). We detail this function and its underlying verification engine later in the chapter. At this point, it is important to note that during the ramping phase, the function will return an enumerated value (not an enum datatype) based on the type of response that Firewalk gets to its probes:


switch (fw_packet_capture(fp))
{

During the ramping phase, the software expects either of the following two responses. We are familiar with both the TTL expired in transit message and the unreachable message from Chapter 9. If the verification engine has determined that the response in question does not bind the scan, Firewalk breaks out of the loop, updates its probe with fw_packet_update_probe(), and continues in the loop. If the scan is bound by the response, Firewalk informs the user and breaks from the ramping loop:


case FW_PACKET_IS_UNREACH_EN_ROUTE:
case FW_PACKET_IS_TTL_EX_EN_ROUTE:
if ((*fp)->flags & FW_BOUND)
{
printf("Binding host reached. n");
done = 1;
}
break;

A terminal response might come at any time and indicates that the metric was reached before the target gateway. This situation means that the target gateway was not en route to the metric, and a different metric should have been chosen. This problem is obviously an unrecoverable situation because Firewalk has nothing to scan. Firewalk sets the loop sentinel done to 1, which results in termination of the ramping phase:


case FW_PACKET_IS_TERMINAL_TTL_EX:
case FW_PACKET_IS_TERMINAL_UNREACH:
case FW_PACKET_IS_TERMINAL_SYNACK:
case FW_PACKET_IS_TERMINAL_RST:
/* any terminal response will end phase one */
done = 1;
break;

If Firewalk encounters an unrecoverable error in the packet capturing or verification module, the following error returns:


case -1:
case FW_SERIOUS_ERROR:
/* err msg set in fw_packet_capture() */
return (FW_SERIOUS_ERROR);

If the user hits Ctrl-c to terminate the program, Firewalk will end up here for a graceful exit:


case FW_USER_INTERRUPT:
/* user hit ctrl-c */
return (FW_USER_INTERRUPT);
}
}

If Firewalk reaches the end of the ramping loop and has not yet bound to a target gateway, it will update its probe template with fw__packet_update_probe() (not shown). During the ramping phase, this function simply bumps up the IP TTL by one each time it is called. We will see that this function is also called in the scanning phase to bump up transport layer ports:


if (!done)
{
if (fw_packet_update_probe(fp, cport) == -1)
{
/* error msg set in fw_packet_update_probe */
return (-1);
}
}
}

The following two checks will catch situations where the target gateway is not en route to the metric and when the hopcount is exceeded. The first happens when the metric is reached and the scan is considered done due to the reception of a terminal response, but the scan is never FW_BOUND because the target gateway was not reached. The hopcount is exceeded whenever the target gateway or metric cannot be reached in IP_HOP_MAX (25) hops.


if (done && !((*fp)->flags & FW_BOUND))
{
/*
* If we're "done" but not "bound" then we hit the metric
* before we hit the target gateway. This means the target
* gateway is not en route to the metric. Game's over kids.
*/
sprintf((*fp)->errbuf,
"metric responded before target; must not be en route");
return (FW_ABORT_SCAN);
}
if (idone)
{
/* if we fall through down here, we've exceeded our hopcount */
sprintf((*fp)->errbuf, "hopcount exceeded");
return (FW_ABORT_SCAN);
}


Stage Three: The Scanning Phase


After a successful ramping phase, Firewalk proceeds to the scanning phase. A flowchart of its operation appears in Figure 12.7.


Figure 12.7: Firewalk scanning phase flowchart.

Like the ramping phase, the scanning phase is also a multi-staged process that revolves around a packet injection, capturing, and processing loop. Again, most of the work is done in the fw_packet_capture() function:

Control passes to the scanning loop after the binding phase is completed. This operation occurs inside the firewalk() function:


/*
* PHASE TWO: Firewalk scanning
* A series of probes are sent to the metric with the bound IP
* TTL. If a given probe is accepted through the target gateway's
* ACL, we will receive an ICMP TTL expired in transit from the
* binding host. If we receive no response after the timeout expires,
* it is assumed the probe violated the ACL on the target and was
* dropped.
*/
(*fp)->ttl += (*fp)->xv;
printf("Scan bound at %d hops.\n", (*fp)->ttl);
printf("Scanning Phase: n");

A sentinel variable controls the flow of the loop:


for (done = 0, i = 0; Idone; i++)
{

In the following block, Firewalk retrieves the next port list pair from the lib-net port list chain. This function returns true as long as there are pairs of ports left to scan. When the list is exhausted, the function returns 0 and Firewalk has completed scanning:


if (!libnet_plist_chain_next_pair{(*fp)->plist, &bport, &eport))
{
/* we've exhausted our portlist and we're done */
done = 1;
continue;
}

The internal scanning loop will scan a series of ports, beginning at bport (the beginning port) and ending with eport (the ending port):


while (!(bport > eport) && bport != 0)
{
eport = bport++;

Firewalk updates the scanning probe with cport (the current port). The fw_packet_update_probe() function (not shown) is used to replace the transport layer destination port with cport rather than modifying the IP header as it did earlier:


if (fw_packet_update_probe(fp, cport) == -1)
{
/* error msg set in fw_packet_update_probe */
return (-1);
}
/* send a series of probes (currently only one) */
for (j = 0; j < 1; j++)
{
fprintf(stderr, "port %3d: ", cport);
(*fp)->stats.ports_total++;

As mentioned earlier, Firewalk shoots the packet into the network:


if (fw_packet_inject(fp) == -1)
{
/*
* Perhaps this write error was transient. We'll
* hope for the best. Inform the user and continue.
*/
fprintf (stderr, "fw_packet_inject(): %s/n",
(*fp)->errbuf);
continue;
}
/* we only care if the return value is an error */

When scanning, the packet processing is handled by fw_packet_capture():


switch(fw_packet_capture(fp))
{
case FW_USER_INTERRUPT:
return (FW_USER_INTERRUPT) ;
case -1:
case FW_SERIOUS_ERROR:
/* err msg set in fw_packet_capture() */
return (FW_SERIOUS_ERROR);

The default case is empty because the processing and reporting is all handled by fw_packet_capture():


default:
/* empty */
}
}
}
}
return (1);
}

After scanning is completed, Firewalk returns control back to the main module:


done:

Before shutting down, Firewalk reports packet injection, capturing, and analysis statistics to the user:


fw_report_stats(&fp);

Firewalk shuts down in an orderly fashion, relinquishing all of its resources back to the operating system (this function is shown as follows):


fw_shutdown(&fp) ;
/* we should probably record proper exit status */

Elvis has left the building:


return (EXIT_SUCCESS);
}

Packet Capturing and Verification


The packet capturing and verification functions perform the majority of Fire-walk's correlation and analysis activity. The fw_packet_capture() function is discussed first, and we diagram it in Figure 12.8.


Figure 12.8: Firewalk packet capture flowchart.

The packet-capturing functionality loops around the select() system call, which is responsible for notifying Firewalk when a packet is ready for processing or when a timeout or error has occurred. If the packet is found to be inconsequential, control passes back to the top in the hopes that another more interesting packet will arrive before the timer expires. If the timeout occurs, Firewalk will move on—recording that it received no suitable response.

Packet capturing occurs inside a loop, and as before, Firewalk needs to use a global sentinel variable to mediate control within the loop. The difference here is that this loop sentinel is global. The interrupt signal handler (called asynchronously) must have a graceful way of shutting down the program. This task occurs by making the sentinel global so that the signal handler (a separate function) can access it:



int loop = 1;
int
fw_packet_capture(struct firepack **fp)
{
int pcap_fd, c, timed_out;
fd_set read_set;
struct timeval timeout;
struct pcap_pkthdr pc_hdr;

Firewalk sets up the select() synchronous multiplexing variables:


timeout.tv_sec = (*fp)->pcap_timeout;
timeout.tv_usec = 0 ;
pcap_fd = pcap_fileno((*fp)->p);
FD_ZERO(&read_set);
FD_SET(pcap_fd, &read_set) ;

The packet-capturing loop starts here. Firewalk will loop until a timeout occurs or when the sentinel variable is set to false, which only happens when the user interrupts the program via Ctrl-c (actually, the interrupt signal might interrupt the select() call itself, which results in it returning -1):


for (timed_out = 0; !timed_out && loop; )
{

Firewalk depends upon the select() facility in order to gracefully multitask between checking for packet responses, read timeouts, and errors:


c = select (pcap_fd + 1, &read_set, 0, 0, &timeout);
switch (c)
{

If a select() error occurs or if the user interrupts the program while Firewalk is inside select(), the following code executes:


case -1:
snprintf((*fp)->errbuf, FW_ERRBUF_SIZE,
"select() %s", strerror(errno));
return (-1);

If no traffic returns within pcap_timeout seconds, select() will time out, return 0, and end up returning FW_NO_REPLY to the scanning module:


case 0:
timed_out = 1;
continue;

The default case is hit when there is a packet to be read from the libpcap device. As a matter of good practice, Firewalk ensures that the libpcap file descriptor is ready for a read operation (which it should be, because it is the only member of the file descriptor read set):


default:
if (FD_ISSET(pcap_fd, &read_set) == 0)
{
timed_out = 1;
continue;
}
/* fall through to read the packet */
}

Firewalk reads in a packet from the libpcap device here:


(*fp) -> packet = (u_char *)pcap_next ((*fp)->p, &pc_hdr);
if ((*fp)->packet == NULL)
{
/* no NULL packets please */
continue;
}
(*fp)->stats.packets_caught++;

At this point, the packet passes to the verification engine, which determines whether the packet is interesting or not. The appropriate verification function calls according to whether or not Firewalk is in the ramping phase or the scanning phase:


/*
* Submit the packet for verification first based on scan type,
* If we're not bound, we're still in phase one and need to
* verify the ramping response. If we are bound, we're in
* phase two and we need to verify the terminal response.
* Then process the response from the verification engine.
* Report to the user if necessary and update the packet
* statistics.
*/
switch (!(((*fp)->flags) & FW_BOUND) ? fw_packet_verify_ramp(fp) :
fw_packet_verify_scan(fp))
{

The results from the verification engine are parsed, and the reporting function fw_report() (not shown) is called. The reporting function simply takes the results from the verification engine and prints a message for the user detailing the packet response type (expired, unreachable, RST, and so on) and the IP address of the host that caused the message. Additionally, with ICMP unreachable packets Firewalk prints the unreachable code:


case FW_PACKET_IS_TTL_EX_EN_ROUTE:
/* RAMPING: TTL expired en route to gateway (standard) */
fw_report(FW_PACKET_IS_TTL_EX_EN_ROUTE, fp);
(*fp)-> stats.packets_caught_interesting++;
return (FW_PACKET_IS_TTL_EX_EN_ROUTE);
case FW_PACKET_IS_UNREACH_EN_ROUTE:
/* RAMPING: Unreachable en route to gateway (uncommon) */
fw_report(FW_PACKET_IS_UNREACH_EN_ROUTE, fp);
(*fp)->stats.packets_caught_interesting++;
return (FW_PACKET_IS_TTL_EX_EN_ROUTE);
case FW_PACKET_IS_TERMINAL_TTL_EX:
/* RAMPING: TTL expired at destination (rare) */
fw_report(FW_PACKET_IS_TERMINAL_TTL_EX, fp);
(*fp) -> stats.packets_caught_interesting++;
return (FW_PACKET_IS_TERMINAL_TTL_EX);
case FW_PACKET_IS_TERMINAL_UNREACH:
/* RAMPING: Unreachable at destination (uncommon) */
fw_report(FW_PACKET_IS_TERMINAL_UNREACH, fp);
(*fp)->stats.packets_caught_interesting++;
return (FW_PACKET_IS_TERMINAL_UNREACH);
case FW_PACKET_IS_TERMINAL_SYNACK:
fw_report(FW_PACKET_IS_TERMINAL_SYNACK, fp);
(*fp)->stats.packets_caught_interesting++;
return (FW_PACKET_IS_TERMINAL_SYNACK);
case FW_PACKET_IS_TERMINAL_RST:
fw_report(FW_PACKET_IS_TERMINAL_RST, fp);
(*fp)->stats.packets_caught_interesting++;
return (FW_PACKET_IS_TERMINAL_RST);
case FW_PORT_IS_OPEN_SYNACK:
/* SCANNING: A response from an open TCP port */
fw_report(FW_PORT_IS_OPEN_SYNACK, fp);
(*fp)->stats.packets_caught_interesting++;
return (FW_PORT_IS_OPEN_SYNACK);
case FW_PORT_IS_OPEN_RST:
/* SCANNING: A response from a closed TCP port */
fw_report(FW_PORT_IS_OPEN_RST, fp);
(*fp)->stats.packets_caught_interesting++;
return (FW_PORT_IS_OPEN_RST);
case FW_PORT_IS_OPEN_UNREACH:
/* SCANNING: A port unreachable response */
fw_report(FW_PORT_IS_OPEN_UNREACH, fp);
(*fp)->stats.packets_caught_interesting++;
return (FW_PORT_IS_OPEN_UNPEACH);
case FW_PORT_IS_OPEN_TTL_EX:
/* SCANNING: A TTL expired */
fw_report(FW_PORT_IS_OPEN_TTL_EX, fp);
(*fp)->stats.packets_caught_interesting++;
return (FW_PORT_IS_OPEN_TTL_EX);
case FW_PACKET_IS_BORING:
default:
continue;
}
}

The signal handler sets the loop sentinel loop to 0 when the user hits Ctrl-c, which we handle here:


if (!loop)
{
return (FW_USER_INTERRUPT);
}

If control reaches this point, select() must have timed out waiting for a response:


/*
* If we get here, the scan timed out. We either dropped a packet
* somewhere or there is some filtering going on.
*/
printf("*no response* n");
fflush(stdout);
return (FW_NO_REPLY);
}

The ramping phase verification function fw_packet_verify_ramp() as shown in Figure 12.9 is detailed as follows.


Figure 12.9: Firewalk packet verification (ramping phase) flowchart.

While the ramping phase verification process might appear confusing at first glance, it is actually relatively straightforward. Firewalk basically just has to make sure that the packet is a response from a probe that it sent, and if so, determine the type of response it is:


int
fw_packet_verify_ramp(struct firepack **fp)
{

Firewalk has to access several different header fields, and as we have seen in earlier chapters, retrieving data from these fields can be done by simply casting the pointers into templates and dereferencing the required fields. As such, Firewalk makes liberal use of libnet's internal IP, ICMP, and TCP header templates:


struct libnet_ipv4_hdr *ip_hdr;
struct libnet_icmpv4_hdr *icmp_hdr;
struct libnet_ipv4_hdr *o_ip_hdr;
struct libnet_tcp_hdr *tcp_hdr;

Initially, Firewalk needs to access the IPv4 header to make sure that it did not generate the packet currently being examined:


/* point to the IP header */
ip_hdr = (struct libnet_ipv4_hdr *)
((*fp)->packet + (*fp)->packet_offset);
if (ip_hdr->ip_src.s_addr == (*fp)->sin.sin_addr.s_addr)
{
/* packets we send are of no interest to us here. */
return (FW_PACKET_IS_BORING);
}

After the sanity check, Firewalk needs to narrow down the packet based on protocol:


switch (ip_hdr->ip_p)
{

ICMP is the standard response packet for the ramping phase.


case IPPROTO_ICMP:
icmp_hdr = (struct libnet_icmpv4_hdr *)
((*fp)->packet + (*fp)->packet_offset + LIBNET_IPV4_H);

ICMP packets, which make up the bulk of the ramping phase, are handled here in the first block:


switch (icmp_hdr->icmp_type)
{

There are two type codes for ICMP TTL expired messages: expired in transit (code 0) and expired in fragmentation reassembly queue (code 1). The latter of the two occurs when an incomplete series of IP fragments times out inside a host's fragmentation queue. Firewalk only wants code 0 ICMP TTL expired messages (of course, the smart play would be to apply this logic to the BPF filter, which would obviate this check):


case ICMP_TIMXCEED:
if (icmp_hdr->icmp_code != ICMP_TIMXCEED_INTRANS)
{
/*
* Packet was from an expired IP frag queue
* reassembly timer. Nothing we want.
*/
break;
}
case ICMP_UNREACH:
/*
* Point to the original IPv4 header inside the ICMP
* message's payload. An IPv4 header is
* LIBNET_IPV4_H bytes long ard both ICMP unreachable
* and time exceed headers are 8 bytes.
*/
o_ip_hdr = (struct libnet_ipv4_hdr *)
((*fp)->packet + (*fp)->packet_offset
+ LIBNET_IPV4_H + 8);

Firewalk needs to check to make sure that the packet is a response to one of its probes. The FW_IS_OURS() macro checks the IPv4 header from ICMP error message's payload to make sure that it contains Firewalk's source IP address and its special IP ID:


/*
* Check the IP header of the packet; that caused the
* unreachable for our markings which include:
* Original IP ID: set to the process id.
* Original IP source address: our source address.
*/
if (!FW_IS_OURS(o_ip_hdr, fp))
{
break;
}

If the packet is a response from the etric host during the ramping phase, the scan is considered finished. Recal from Chapter 9 that when an ICMP unreachable message is generated dur_-g an IP expiry scan in response to a UDP packet, it is a terminal packet. The same holds true (in this case) for TTL expired in transit messages. Firewalk returns the terminal response, which results in program shutdown:


if (ip_hdr->ip_src.s_addr == (*fp)->metric)
{
/*
* ICMP response from our metric. This ends
* our scan since we've reached the metric
* before the target gateway.
*/
return ((icmp_hdr->icmp_type == ICMP_TIMXCEED) ?
FW_PACKET_IS_TERMINAL_TTL_EX:
FW_PACKET_IS_TERMINAL_UNIEACH);
}

If the packet is a response from the target gateway during the ramping phase, Firewalk attempts to start scanning. TIL expired in transit messages from target gateways are considered normal while unreachable messages are often indicative of prohibitive filtering. Because an unreachable from the target gateway message is not the expected TTL expired in transit message, results of the scanning phase might vary. Firewalk binds the scan, which results in the program moving to the scanning phase:


if (ip_hdr->ip_src.s_addr == (*fp)->gateway)
{
/*
* Response from our target gateway.
*/
(*fp)->flags |= FW_BOUND;
}

If control falls through down here, the packet is a response from an intermediate router. Firewalk treats it as a normal intermediate host and returns the response resulting in the ramping phase continuing:


/*
* If we get to this point, the packet is an
* ICMP response from an intermediate router.
*/
return ((icmp_hdr->icmp_type == ICMP_TIMXCEED) ?
FW_PACKET_IS_TTL_EX_EN_ROUTE:
FW_PACKET_IS_UNREACH_EN_ROUTE);
break;
default:
break;
}

TCP packets, which should only be encountered when Firewalk is using the TCP protocol and only then when the metric is reached, are handled here:


case IPPROTO_TCP:

Firewalk first makes a quick sanity check to ensure that the current session uses TCP:


if ((*fp)->protocol != IPPROTO_TCP)
{
/*
* We're only interested in TCP packets if this is a
* TCP-based scan.
*/
break;
}
tcp_hdr = (struct libnet_tcp_hdr *)
((*fp)->packet +
(*fp)->packet_offset + LIBNET_IPV4_H);

Firewalk scans by using SYN packets, and as such, only SYN|ACK and RST packets should be returned:


if (!(tcp_hdr->th_flags & TH_SYN) &&
!(tcp_hdr->th_flags & TH_RST))
{
/*
* We only care about SYN|ACK and RST|ACK packets.
* The rest can burn.
*/
break;
}

According to RFC 793, an RST or an SYN|ACK will have the sequence number of the sender +1 as its acknowledgement number (and an RST packet will have the ACK bit set). When strict RFC checking is enabled (via the -r switch at the command line), Firewalk will enforce this policy on TCP-based scans and will ignore response packets that do not meet this criterion (that it would otherwise accept):


if ((*fp)->flags & FW_STRICT_RFC)
{
/*
* Strict RFC compliance dictates that an RST or
* an SYN|ACK will have our SEQ + 1 as the ACK number
* also, the RST will have the ACK bit set) . This is of
* course, assuming the packet is ours.
*/
if (ntohl(tcp_hdr->th_ack) != (*fp)->seq + 1)
{
break;
}
}

Firewalk checks the TCP tuple information to make sure that this packet is a response to its probe:


if (ntohs(tcp_hdr->th_dport) == (*fp)->sport &&
ntohs(tcp_hdr->th_sport) == (*fp)->dport)
{

As noted earlier, a TCP response from the metric is always a terminal one. Firewalk returns the terminal response, which results in program shutdown:


/* this is most likely a response to our SYN probe */
return (((tcp_hdr->th_flags & TH_SYN) ?
FW_PACKET_IS_TERMINAL_SYNACK :
FW_PACKET_IS_TERMINAL_RST));
}
break;
}

If control reaches here, the packet is not a response to a Firewalk probe:


return (FW_PACKET_IS_BORING);
}

The scanning phase verification function fw_packet_verify_scan() appears in Figure 12.10.


Figure 12.10: Firewalk packet verification (scanning phase) flowchart.

The scanning phase verification process is simpler than the ramping phase process. Again, Firewalk has to make sure that the packet is a response from a probe that it sent, but due to the more limited nature of scanning, the types of responses that it must account for form a narrow set.

fw_packet_verify_scan() has almost identical logic as the ramping phase verification function, with the only difference being that once an ICMP or TCP packet is verified to be a response to a Firewalk probe, Firewalk returns a different code to the fw_packet_capture() function indicative of the type of packet that was received. The code for dealing with UDP-based scans is as follows:


if (FW IS OURS(o_ip_hdr, fp) )
{
/* the packet made it through the filter */
return ((icmp_hdr->icmp_type == ICMP_TIMXCEED) ?
FW_PORT_IS_OPEN_TTL_EX :
FW_PORT_IS_OPEN_UNREACH);
}

The code for dealing with TCP-based scans is as follows:


if (ntohs(tcp_hdr->th_dport) == (*fp)->sport &&
ntohs(tcp_hdr->th_sport) == (*fp)->dport)
{
/* the packet made it through the filter */
return (((tcp_hdr->th_flags & TH_SYN) ?
FW_PORT_IS_OPEN_SYNACK :
FW_PORT_IS_OPEN_RST));
}


Shutdown


Anytime an error is encountered, the user hits Ctrl-c (which sends an interrupt signal). Or, if the program completes successfully, Firewalk exits gracefully with the following function:


void
fw_shutdown(struct firepack **fp)
{

There are times when you could call this function with a NULL fp pointer. To avoid any messy segmentation violations, Firewalk checks to ensure that fp is a valid pointer:


if (*fp)
{

Firewalk then systematically shuts down and frees all of the states associated with fp, including the libnet and libpcap contexts as well as the libnet port list chain:


if ((*fp)->p)
{
pcap_close((*fp)->p);
}
if ((*fp)->1)
{
libnet_destroy((*fp)->1);
}
if ((*fp)->plist)
{
libnet_plist_chain_free((*fp)->plist);
}

Finally, the memory associated with fp is freed. The pointer is set to NULL to avoid any dangling pointer issues:


free(*fp);
*fp = NULL;
}
}

/ 135