Nmap Development mailing list archives

Nmap (4.76) out-of-bounds memory access bug & patch


From: "ithilgore.ryu.L () gmail com" <ithilgore.ryu.l () gmail com>
Date: Fri, 21 Nov 2008 03:33:33 +0200

Hey everyone.

I've been looking at the nmap source code lately and found a nasty
out-of-bounds memory access bug due to insufficient validation of packet
lengths. The problem is a bit complex so I 'll start describing it step by step. Note that is applies to the latest Nmap version: 4.76. In the end I 've included a patch that eliminates the bug.

------------------------
0x01. Bug Description
0x02. Bug Triggering
0x03. Debugging Session
0x04. Patch
-------------------------

0x01. Bug Description
======================

The bug lies in functions readip_pcap, validatepkt and validateTCPhdr inside the tcpip.cc file. Let's first examine readip_pcap():

Before calling validatepkt(), readip_pcap() computes the length of the IP datagram (let's assume that we are dealing with TCP/IP packets) and allocates a buffer according to this length (*len). After that, it passes the address of the buffer (alignedbuf) and *len to validatepkt().


code snippet from readip_pcap()
--------------------------------------------------------------

 *len = head.caplen - offset;
 if (*len > alignedbufsz) {
   alignedbuf = (char *) safe_realloc(alignedbuf, *len);
   alignedbufsz = *len;
 }
 memcpy(alignedbuf, p, *len);

 if (validate) {
   /* Let's see if this packet passes inspection.. */
   if (!validatepkt((u8 *) alignedbuf, *len)) {
     *len = 0;
     return NULL;
   }
--------------------------------------------------------------


Let's move on to validatepkt():


code snippet from validatepkt()
--------------------------------------------------------------
bool validatepkt(u8 *ipc, unsigned len)
{
        struct ip *ip = (struct ip *) ipc;
        unsigned fragoff, iphdrlen, iplen;

        ...

        iphdrlen = ip->ip_hl * 4;

        ...

        iplen = ntohs(ip->ip_len);

        if (iplen < iphdrlen) {
                if (o.debugging >= 3)
                        error("Rejecting IP packet because of invalid total length %u", iplen);
                return false;
        }

        fragoff = 8 * (ntohs(ip->ip_off) & IP_OFFMASK);

        if (fragoff) {
                if (o.debugging >= 3)
                        error("Rejecting IP fragment (offset %u)", fragoff);
                return false;
        }

        switch (ip->ip_p) {
        case IPPROTO_TCP:
                if (iphdrlen + sizeof(struct tcp_hdr) > iplen) {
                        if (o.debugging >= 3)
                                error("Rejecting TCP packet because of incomplete header");
                        return false;
                }
                if (!validateTCPhdr(ipc + iphdrlen, iplen - iphdrlen)) {
                        if (o.debugging >= 3)
                                error("Rejecting TCP packet because of bad TCP header");
                        return false;
                }

        ...

        return true;
}
--------------------------------------------------------------


The problem starts here:

        iplen = ntohs(ip->ip_len);

This alone, looks perfectly sane. Just assign the value of the ip_len field of the IP header to iplen. This means, however, that we actually *trust* that this field has the correct length. Let's move on. Since we are dealing with TCP packets, validatepkt() will later call validateTCPhdr() like this:

        if (!validateTCPhdr(ipc + iphdrlen, iplen - iphdrlen)) {

ipc is the same address that we had passed to validatepkt() from readip_pcap() as alignedbuf.

iphdrlen is the length of the IP header which will normally be 20

iplen is the total length of the IP datagram as we mentioned before.

So validateTCPhdr is passed an address that shows to the beginning of the TCP header, and a length that is normally the length of the TCP header + it's data (since that is what entails the subtraction: iplen - ipdhrlen).

Moving on inside validateTCPhdr():

code snippet from validateTCPhdr()
--------------------------------------------------------------
bool validateTCPhdr(u8 *tcpc, unsigned len)
{
        struct tcp_hdr *tcp = (struct tcp_hdr *) tcpc;
        unsigned hdrlen, optlen;

        hdrlen = tcp->th_off * 4;

        /* Check header length */
        if (hdrlen > len || hdrlen < sizeof(struct tcp_hdr))
                return false;

        /* Get to the options */
        tcpc += sizeof(struct tcp_hdr);
        optlen = hdrlen - sizeof(struct tcp_hdr);
--------------------------------------------------------------

hdrlen is assigned the length of the TCP header. After that the code makes sure that it is not more than the TCP segment length (as defined in len) and less than 20 (which is the normal TCP header length). Then it calculates optlen so that it knows how many TCP options to parse inside this loop:


code snippet from validateTCPhdr()
--------------------------------------------------------------
        while (optlen > 0) {
                switch (*tcpc) {
                case 2: /* MSS */
                        if (optlen < 4)
                                return false;
                        optlen -= 4;
                        tcpc += 4;
                        break;
                case 3: /* Window Scale */
                        if (optlen < 3)
                                return false;
                        optlen -= 3;
                        tcpc += 3;
                        break;
                case 4: /* SACK Permitted */
                        if (optlen < 2)
                                return false;
                        optlen -= 2;
                        tcpc += 2;
                        break;
                case 5: /* SACK */
                        if (optlen < *++tcpc)
                                return false;
                        if (!(*tcpc - 2) || ((*tcpc - 2) % 8))
                                return false;
                        optlen -= *tcpc;
                        tcpc += (*tcpc - 1);
                        break;
                case 8: /* Timestamp */
                        if (optlen < 10)
                                return false;
                        optlen -= 10;
                        tcpc += 10;
                        break;
                default:
                        optlen--;
                        tcpc++;
                        break;
                }
        }
--------------------------------------------------------------

Notice that it actually dereferences the tcpc in order to check the actual values of the TCP options.

Now it is time to look how the bug is triggered:

0x02. Bug triggering
=====================

The fact is that validatepkt() does *NOT* check if the real IP datagram length is equal to the ip->ip_len field. The real IP datagram length is what has been calculated before (and libpcap passes the *real* length always) and passed to validatepkt() as *len. So imagine that I have sent as reply to a scanning, a packet that is like this:


-------- special packet attributes---------------------

Actual data: 33 bytes (of whole IP datagram)
33 is not really random - it is actually just enough to have a full IP header (20 bytes) and the tcp->th_off to be inside the packet, since it is the 4-bit value in byte 12 of TCP header) This can be 40 bytes as well. (since we are going to need to have the full TCP header if we are to pass the checksum checks that the intermediary routers will do - for now suppose that the attacked host is on the same subnet as the attacking) It doesn't matter actually, since libpcap will pass a length value that is more than that anyway. But that doesn't prevent the bug triggering as we will later see.


IP header length: 20 bytes (ip->ip_hl = 5 since this will be multiplied by 4)

IP total length field (in IP header): 80 bytes
This is the minimum value we want to pass the check in validateTCPhdr.

TCP header length: 60 bytes (tcp->th_off = 15 since this will be multiplied by 4) (This is actually the maximum value we can assign since th_off is an unsigned 4 bit value)

-------- special packet attributes---------------------



After we send the above packet as a reply to an attacker running nmap, then this is what happens:

readip_pcap():

 *len = head.caplen - offset;
 if (*len > alignedbufsz) {
   alignedbuf = (char *) safe_realloc(alignedbuf, *len);


len will be 46 bytes maximum(as far as debugging tells). Why isn't it 33 bytes? I guess that has to do with libpcap and a minimum returning value (this must be 60 bytes since offset is normally 14 on ethernet). However, it is still a small value.


validatepkt() will not complain about anything out-of-the-ordinary since the checkes it does, since the only check that concerns iplen is this:

        if (iplen < iphdrlen) {
                if (o.debugging >= 3)
                        error("Rejecting IP packet because of invalid total length %u", iplen);
                return false;
        }

Our packet gracefully passes the above check since iplen (80) is greater than iphdrlen(20). Then validateTCPhdr is called and the argument len it gets as argument comes from: iplen - iphdrlen = 80 - 20 = 60

So the variables inside validateTCPhdr will take the values below:

len = 60
hdrlen = 60
optlen = hdrlen - sizeof(struct tcp_hdr) = 60 - 20 = 40

Note that the check done by validateTCPhdr() does not fail since we are just inside the bounds it checks:

        /* Check header length */
        if (hdrlen > len || hdrlen < sizeof(struct tcp_hdr))
                return false;

hdrlen (60) is equal to len (60) and more than 20.

So, we've passed all the checks and we have 40 byte options to parse from the TCP header. But let's remember how much space we had allocated for the buffer that holds the packet. The *whole* buffer was 46 bytes. And we are already on the 40 first bytes since the pointer tcpc is 20 bytes above the IP header whose end was 20 bytes above the beginning of the buffer.

        tcpc += sizeof(struct tcp_hdr);

Thus, we have 40 bytes to parse/dereference from 6 bytes that are our own and 34 bytes from memory that is *not* ours!

This can result to a possible segmentation fault or worse.


Now there is a tricky part that I have to mention here. The Linux kernel does not let you by default create packets that have an arbitrary ip_len field in the IP header. So I compiled my own custom kernel to let me do this. The only change is to comment out this line from /usr/src/linux-2.6.26/net/ipv4/raw.c in function raw_send_hdrinc:

        iphlen = iph->ihl * 4;
        if (iphlen >= sizeof(*iph) && iphlen <= length) {
                if (!iph->saddr)
                        iph->saddr = rt->rt_src;
                iph->check   = 0;
                // iph->tot_len = htons(length);
                if (!iph->id)
                        ip_select_ident(iph, &rt->u.dst, NULL);

Note that FreeBSD lets you do whatever you wish with the IP header and no kernel change is needed in order to arbitrarily create a fake tot_len field.



The following gdb sesssion proves the above:

0x03. Debugging Session
========================

To be able to debug the whole thing, I scanned a specific port from a host with nmap 4.76 and replied from that host with a custom tool that made the special attribute packets mentioned above. I of course run nmap inside gdb and had placed the nessecary breakpoints.
Let's see what gdb says:


Breakpoint 1, validatepkt (ipc=0x9cfa640 "E", len=46) at tcpip.cc:1956
1956            struct ip *ip = (struct ip *) ipc;

Notice that ipc is the address of alignedbuf and marks the beginning of the allocated buffer.

... steps ...

(gdb) p iplen
$31 = 80

validateTCPhdr (tcpc=0x9cfa654 "", len=60) at tcpip.cc:1883
1883            struct tcp_hdr *tcp = (struct tcp_hdr *) tcpc;

Notice that now tcpc shows 0x14 = 20d bytes after alignedbuf.

(gdb) step
1886            hdrlen = tcp->th_off * 4;
(gdb)
1889            if (hdrlen > len || hdrlen < sizeof(struct tcp_hdr))
(gdb)
1893            tcpc += sizeof(struct tcp_hdr);
(gdb)
1894            optlen = hdrlen - sizeof(struct tcp_hdr);
(gdb)
1896            while (optlen > 0) {
(gdb) p hdrlen
$32 = 60
(gdb) p optlen
$33 = 40
(gdb) p tcpc
$34 = (u8 *) 0x9cfa668 ""

And now notice that optlen = 40 and tcpc shows 40d bytes after alignedbuf.

Let's see how far tcpc will go until the while loop finishes.

... many steps ...

1896            while (optlen > 0) {
(gdb)
1897                    switch (*tcpc) {
(gdb)
1905                            if (optlen < 3)
(gdb)
1906                                    return false;
(gdb) p tcpc
$36 = (u8 *) 0x9cfa68e "\003"

Now the loop happened to terminate a bit earlier because there was a random value of 3 somewhere on the memory it accessed (but optlen was already < 3). It could go on a bit more if that didn't occur.

alignedbuf: start: 0x9cfa640 end: 0x9cfa640 + 46d = 0x9cfa66e
tcpc (at end of loop): 0x9cfa68e

We 've accessed 0x9cfa68e - 0x9cfa66e = 32 bytes that are not our own.

Apart from the above out of bound mem access, the missing validation check that is mentioned in the patch part, could also possibly cause problems elsewhere on the code. I haven't checked for more end-cases but they may be possible.


0x04. Patch
============

The patch is simple to implement. We just need to make an additional check to ensure that ip->ip_len is equal or less than the actual data length that is returned by libpcap. This can be placed in validatepkt() like this:


*** tcpip.cc    Fri Nov 21 03:26:16 2008
--- _tcpip.cc   Fri Nov 21 02:36:16 2008
***************
*** 1978,1984 ****

        iplen = ntohs(ip->ip_len);

!       if (iplen < iphdrlen || len < iplen) {
                if (o.debugging >= 3)
error("Rejecting IP packet because of invalid total length %u", iplen);
                return false;
--- 1978,1984 ----

        iplen = ntohs(ip->ip_len);

!       if (iplen < iphdrlen) {
                if (o.debugging >= 3)
error("Rejecting IP packet because of invalid total length %u", iplen);
                return false;


Of course, if we want to be more strict we could also reject any packet that has len != iplen. But the above check is enough so that no troubles are caused on our side.

I hope my analysis was clear enough.

Cheers,
ithilgore (sock-raw.homeunix.org)



_______________________________________________
Sent through the nmap-dev mailing list
http://cgi.insecure.org/mailman/listinfo/nmap-dev
Archived at http://SecLists.Org


Current thread: