Fuzzing Vulnserver: Discovering Vulnerable Commands: Part 1

Introduction

Vulnserver is a Windows TCP server running on port 9999. It was written by Stephen Bradshaw whose blog is located here. The server was intentionally written to be vulnerable, for the purpose of learning how to fuzz a real target. The problem is that when we're learning, we expect results, so if we try to fuzz some updated FTP/HTTP server, or something else entirely, we will most probably fail and give up. When learning, we need a server program that we know contains vulnerabilities, so we are able to find them with fuzzing process.

We can download Vulnserver from Stephen Bradshaws' blog. The zip file contains the actual server named vulnserver.exe, which depends upon essfunc.dll which is also included. Besides that, the Vulnserver contains the actual C source code, which we can modify, delete, add and compile into the executable again.

Discovering security vulnerabilities with source code auditing

This step is mandatory for successful result auditing when the fuzzing process is finished. If we don't know all the vulnerabilities that a program has, how can we determine how good a fuzzer is? The answer is very simple: we can't. If we don't know all of the vulnerabilities, we can't determine how good a fuzzer is. That is why we'll audit the source code of a Vulnserver program to find all present vulnerabilities.

The Vulnserver comprises of vulnserver.c and essfunc.c source files. We can quickly identify the functions used in the vulnserver.c that use unsafe C functions, which are responsible for buffer overflow vulnerabilities present in the program. The vulnerable functions are the following ones:

void Function1(char *Input) {
        char Buffer2S[140];
        strcpy(Buffer2S, Input);
}

void Function2(char *Input) {
        char Buffer2S[60];
        strcpy(Buffer2S, Input);
}

void Function3(char *Input) {
        char Buffer2S[2000];
        strcpy(Buffer2S, Input);
}

void Function4(char *Input) {
        char Buffer2S[1000];
        strcpy(Buffer2S, Input);
}

We can see that all four functions: Function1, Function2, Function3 and Function4 use vulnerable C function strcpy. The prototype of the strcpy function is presented here:

char * strcpy ( char * destination, const char * source );

We can see that the strcpy function copies the C string pointed by the source into the array pointed by destination, including the terminating null character. But the function doesn't check if the array pointed by destination is long enough to contain the same C string as source (including the terminating null character). So, if the input parameter source contains a string that is longer than the destination array can hold, a buffer overflow will occur, overwriting other data structures in the memory. A safer way of copying the string from one buffer to the other would be to use strncpy C function, which also takes the maximum length of the string to be copied from one buffer to the other, ensuring that the rest of the string isn't copied from source to destination if the size of the destination array isn't long enough.

Let's also identify the maximum number of bytes that can be inputed as an Input parameter into each of the vulnerable functions that buffer overflow doesn't occur. Each of the functions can accept the following number of bytes:

  • Function1: 140 bytes
  • Function2: 60 bytes
  • Function3: 2000  bytes
  • Function4: 1000 bytes

Ok, we've identified the vulnerable functions, but we have to backtrack the execution of the program to see whether they actually get used in an unsafe manner. We must identify where the vulnerable functions are called. Let's take a look at the following piece of code:

else if (strncmp(RecvBuf, "KSTET ", 6) == 0) {
        char *KstetBuf = malloc(100);
        strncpy(KstetBuf, RecvBuf, 100);
        memset(RecvBuf, 0, DEFAULT_BUFLEN);
        Function2(KstetBuf);
        SendResult = send( Client, "KSTET SUCCESSFUL\n", 17, 0 );
}

We can see that if the received buffer starts with a static string “KSTET ” (note the whitespace after the KSTET command), the if clause is visited and the presented block is executed. The received buffer is a buffer that is sent from the client to the user – if we connect to the Vulnserver on port 9999 and send the command KSTET with a certain parameter to the server, that would represent the received buffer.

Ok, so the beginning part of the received buffer should be “KSTET ”. After that we allocate 100 bytes of memory, copy 100 bytes from the received buffer to the newly initialized buffer and reset the received buffer to zeros. The call to the vulnerable function follows, which is:

Function2(KstetBuf);

That calls the vulnerable function Function2 with an input argument of the newly created array, which holds 100 bytes. Before we've seen that Function2 can only accept 60 bytes, so an overflow of 40 bytes occurs.

So we've identified the vulnerable part of the program, from the input data to the buffer overflow. Now we need to identify all vulnerable commands – like KSTET above – to be able to later analyze the results of the fuzzer.

Vulnerable commands are represented in the following pictures, where the first few bytes constitute a command and a whitespace, the black color specifies a safe data block and the orange color specifies the data block which overwrites the reserved memory, thus causing a buffer overflow.

Buffer overflow vulnerability in GMON command.

Buffer overflow vulnerability in GMON command.

Buffer overflow vulnerability in LTER command.

Buffer overflow vulnerability in LTER command.

Buffer overflow vulnerability in GTER command.

Buffer overflow vulnerability in GTER command.

Buffer overflow vulnerability in HTER command.

Buffer overflow vulnerability in HTER command.

Buffer overflow vulnerability in KSTET command.

Buffer overflow vulnerability in KSTET command.

The individual white bytes in the black area are certain bytes that need to be present in the input argument for one of the vulnerable functions to be called. This can be seen in the TRUN command:

else if (strncmp(RecvBuf, "TRUN ", 5) == 0) {
  char *TrunBuf = malloc(3000);
  memset(TrunBuf, 0, 3000);
  for (i = 5; i < RecvBufLen; i++) {
    if ((char)RecvBuf[i] == '.') {
      strncpy(TrunBuf, RecvBuf, 3000);
      Function3(TrunBuf);
      break;
    }
  }
  memset(TrunBuf, 0, 3000);
  SendResult = send( Client, "TRUN COMPLETE\n", 14, 0 );
}

We can see that there is an additional check if some character in the receive buffer equals to dot (char '.'), then call Function3 with an overly long input buffer.

Similar vulnerability lies in command LTER; the appropriate code is presented here:

if (strncmp(RecvBuf, "LTER ", 5) == 0) {
  char *LterBuf = malloc(DEFAULT_BUFLEN);
  memset(LterBuf, 0, DEFAULT_BUFLEN);
  i = 0;
  while(RecvBuf[i]) {
    if ((byte)RecvBuf[i] > 0x7f) {
      LterBuf[i] = (byte)RecvBuf[i] - 0x7f;
    } else {
      LterBuf[i] = RecvBuf[i];
    }
    i++;
  }
  for (i = 5; i < DEFAULT_BUFLEN; i++) {
    if ((char)LterBuf[i] == '.') {
      Function3(LterBuf);
      break;
    }
  }
  memset(LterBuf, 0, DEFAULT_BUFLEN);
  SendResult = send( Client, "LTER COMPLETE\n", 14, 0 );
}

We can see that the code block first copies the received buffer into LterBuf variable, where it subtracts 0x7f from the bytes that are larger that 0x7f, this is probably just so that we'd have a hard time developing an exploit, since we can't use 0x80, which corresponds to a software interruption. After that the code block calls vulnerable function Function3 if at least one byte in the received buffer contains the dot character – character '.'. Another special command is GMON, which requires a character '/' in the received buffer. Its corresponding code is presented here:

if (strncmp(RecvBuf, "GMON ", 5) == 0) {
  char GmonStatus[13] = "GMON STARTED\n";
  for (i = 5; i < RecvBufLen; i++) {
    if ((char)RecvBuf[i] == '/') {
      if (strlen(RecvBuf) > 3950) {
        Function3(RecvBuf);
      }
      break;
    }
  }
  SendResult = send( Client, GmonStatus, sizeof(GmonStatus), 0 );
}

We're checking if the received buffer contains character '/' and if the length of the received buffer is larger than 3950 bytes, then calling Function3. So, we have to send a buffer, which is at least 3951 bytes long to the Vulnserver, for buffer overflow to occur.

The second last vulnerability happens if an overly long parameter is passed to GTER command, which code is presented below:

if (strncmp(RecvBuf, "GTER ", 5) == 0) {
  char *GterBuf = malloc(180);
  memset(GdogBuf, 0, 1024);
  strncpy(GterBuf, RecvBuf, 180);
  memset(RecvBuf, 0, DEFAULT_BUFLEN);
  Function1(GterBuf);
  SendResult = send( Client, "GTER ON TRACK\n", 14, 0 );
}

We can see that this function isn't complicated; all it does is copy the first 180 bytes of received buffer to a temporary buffer named GterBuf and passes that to the vulnerable function Function1, where the buffer overflow occurs.

The last vulnerability lies in the code block of the HTER command, which code is represented below:

else if (strncmp(RecvBuf, "HTER ", 5) == 0) {
  char THBuf[3];
  memset(THBuf, 0, 3);
  char *HterBuf = malloc((DEFAULT_BUFLEN+1)/2);
  memset(HterBuf, 0, (DEFAULT_BUFLEN+1)/2);
  i = 6;
  k = 0;
  while ( (RecvBuf[i]) && (RecvBuf[i+1])) {
    memcpy(THBuf, (char *)RecvBuf+i, 2);
    unsigned long j = strtoul((char *)THBuf, NULL, 16);
    memset((char *)HterBuf + k, (byte)j, 1);
    i = i + 2;
    k++;
  }
  Function4(HterBuf);
  memset(HterBuf, 0, (DEFAULT_BUFLEN+1)/2);
  SendResult = send( Client, "HTER RUNNING FINE\n", 18, 0 );
}

This code block is quite complicated compared to other code blocks. The call to vulnerable function Function4 happens after the while loop, so it isn't really that important what the while loop does; all that is important when fuzzing is detecting the vulnerability. If we would be writing an exploit that would abuse the HTER vulnerability then we would of course need to know the details of the while loop, since it would change our payload attack that wouldn't work when reassembled.

We've identified all vulnerabilities present in all of the commands defined in the Vulnserver TCP server program. Let's present the commands again, this time listed with the maximum number of bytes the parameter can hold to not cause buffer overflow:

  • KSTET: 60 bytes
  • TRUN: 2000 bytes plus the input argument must contain character '.'
  • GMON: 3950 because of an additional check in the source code plus the input argument         must contain character '/'.
  • GTER: 140 bytes
  • HTER: 1000 bytes
  • LTER: 2000 bytes plus the input argument must contain character '.'

Running the Vulnserver

Let's run the Vulnserver to identify all possible commands present in the TCP server program. To run the server, we can just execute the program normally under Windows:

C:\> vulnserver.exe
Starting vulnserver version 1.00
Called essential function dll version 1.00

This is vulnerable software!
Do not allow access from untrusted systems or networks!

Waiting for client connections...

We can see that the Vulnserver started ok and it is waiting for client connections. We can connect to it via the telnet program:

# telnet 192.168.1.127 9999
Trying 192.168.1.127...
Connected to 192.168.1.127.
Escape character is '^]'.
Welcome to Vulnerable Server! Enter HELP for help.

We've successfully connected to the Vulnserver TCP server program. We can see that the Vulnserver is asking us to enter a HELP command to display its help. Ok, let's do it; when we enter the HELP command, the following appears:

Valid Commands:

HELP
STATS [stat_value]
RTIME [rtime_value]
LTIME [ltime_value]
SRUN [srun_value]
TRUN [trun_value]
GMON [gmon_value]
GDOG [gdog_value]
KSTET [kstet_value]
GTER [gter_value]
HTER [hter_value]
LTER [lter_value]
KSTAN [lstan_value]
EXIT

We've identified all possible Vulnserver commands, which are: HELP, STATS, RTIME, LTIME, SRUN, TRUN, GMON, GDOG, KSTET, GTER, HTER, LTER, KSTAN and EXIT, among which the following are vulnerable: KSTET, TRUN, GMON, GTER, HTER and LTER.

Conclusion

In the next article we'll try to fuzz the Vulnserver to detect the vulnerable commands: KSTET, TRUN, GMON, GTER, HTER and LTER. Since we now know the vulnerable commands, we can try our fuzzer and see whether it can detect those vulnerabilities or not. The best fuzzer in the world would detect all vulnerabilities that exist in a certain software product, but since we don't know how many undetectable vulnerabilities are just waiting to be discovered in a real world application, we can't use that for our fuzzing purposes.

Instead, we can use Vulnserver as our fuzzing target that contains exactly six vulnerabilities, which our fuzzer needs to detect. The more vulnerabilities that a fuzzer will detect, the better the fuzzer is.

Comments