Using enumerations

In C and C++, enumerated types, also called enumerations, can greatly increase the readability and maintainability of source code. However, using enumerations improperly can cause security issues.

Best practices

You can use the following guidelines to help reduce the risk of security issues when using enumerated types:

  • Enumerated types in C and C++ might be signed or unsigned depending on the compiler.
  • Any integer value is valid, not just those defined by a symbol.
  • Check the compiler documentation for command-line switches that might change the way enumerated types are handled.
  • Check that a value is in a legal range and ensure that there is both an upper and lower bounds check.
  • When using switch statements, make sure that there is a default clause.
  • When using switch statements, make sure that variables set by some cases are initialized before the switch.
  • Use enumerated types for a single contiguous range of values.
  • When using multiple discrete values, consider using #define rather than enum.

Enumerated types

Enumerated types assign constant integer values to symbols. According to the C standard, " the expression that defines the value of an enumeration constant shall be an integer constant expression that has a value representable as an int. " However, the implementation is compiler-dependent, so the same source compiled with two different compilers could behave differently. The QNX QCC and GCC compilers use unsigned values by default. If you are developing apps for multiple platforms and using other compilers, you should check the compiler documentation for implementation-specific information and keep in mind the unsigned nature of default enumerated values.

If a compiler uses unsigned integers by default, specifying a negative value will cause the compiler to use signed integers. Conversely, if a compiler uses signed integers, it's not possible to create an unsigned enumeration by, for example, setting one element of the enumeration to a value (such as 0xffffffff) that's too large for a signed integer.

To demonstrate this, consider the following example:

#include <stdio.h>

int main(void) {    
     enum Test1 { a, b, c };    
     enum Test2 { x = 1, y = 65536, z = 0xffffffff };

     printf("a = %d, b = %d, c = %d\n", a, b, c); 
                           
     if (z < 0)
         printf("Signed: x = %d, y = %d, z = %d\n", x, y, z);
     else 
         printf("Unsigned: x = %u, y = %u, z =  %u\n", x, y, z);
     return 0;
}

When this code is compiled with the GCC and run, the following output is produced:

a = 0, b = 1, c = 2
Signed: x = 1, y = 65536, z = 4294967295

Because enumerated types are integers, the guidance provided in Using integers applies. While arithmetic isn't particularly common with enumerated types, arithmetic is legal code and is often used in bounds checking.

Common issues

Some problems that are commonly found in code are:

  • Missing or incomplete bounds checking
  • Missing cases and default clauses in switch statements
  • Confusion between contiguous and discrete values

The following sections explore examples of each issue in detail. Each example uses a simple but common protocol to illustrate the issue. This protocol consists of chunked data in the following form:

[TYPE][DATA][TYPE][DATA][TYPE][DATA]…

[TYPE] is a 32-bit enumerated type that indicates the type, length, and other potential properties of the corresponding data portion. The source of the protocol might be network packets, files, or other untrusted media.

Bounds checking

A common coding error occurs if the upper bound of a value is checked before the value is used in a risky way (for example, using the value as an index into an array of lengths or pointers) in a signed enumerated type. You can avoid this type of flawed check by specifying a negative value.

The following example shows how an incorrectly bounds-checked value can be used to alter the length of a copy. Current memory protection technologies do not mitigate this kind of attack.

typedef enum PACKET_TYPE { 
     CONNECT,
     AUTHENTICATE,
     TRANSFER,
     DISCONNECT,
     MAX
};
        
int Lengths[] = { 16, 128, 256, 8 };
        
void Process(unsigned char * packet, int length) {
     unsigned char work[256];
     unsigned char * p = packet;
     PACKET_TYPE type;
     int n;
            
     while ((p - packet) < length) {
          type = *(PACKET_TYPE *)p;
          p += sizeof(PACKET_TYPE);
                
          if (type < MAX) {
               n = lengths[type];
               memcpy(work, p, n);
               p += n;
                    
               // ...
          }
     }
}

The result of incorrect bounds checking will vary depending on the value supplied for type.

Condition

Result

Index points to unmapped or unreadable memory

Crash due to a read access violation or translation fault

Index points to a very large value

Crash due to a read or write access violation or translation fault caused by unmapped memory being used as either source or destination in memcpy() before completion

Index points to a value slightly larger than 256

Code execution resulting from a stack overflow

The following code sample shows how a similar issue can occur when type is used as an index into a table of function pointers, where each function handles one or more types of data and returns the length of data it processed.

typedef int (*HANDLER)( unsigned char *);

HANDLER Handlers[] =
{
     DoConnect,
     DoAuth,
     DoXfer,
     DoDisconnect
};
        
void Process(unsigned char * packet, int length) {
     unsigned char * p = packet;
     PACKET_TYPE type;
            
     while ((p - packet) < length) {
          type = *(PACKET_TYPE *)p;
          p += sizeof(PACKET_TYPE);
                
          if (type < MAX) {
               p += Handlers[type](p);
                    
               // ... 
          }
     }
}

The result in this case will also depend on the value supplied for type.

Condition

Result

Index points to unmapped or unreadable memory

Crash due to a read access violation or translation fault

Value at the index points to unmapped, unreadable, or non-executable memory

Crash due to a read access violation or translation fault

Pointer at the index points to executable memory

Code execution when the handler is called

The correct way to perform the validation in both of the preceding examples is to check both the upper and lower bound for values. Consider the following check that's performed in the examples:

if (type < MAX)

You should replace this check with the following check:

if (type >= 0 && type < MAX)

Switch statements

A common issue when using switch statements with enumerated types is assuming that the switch statement tests only for values defined as members of the enumeration. Often, any integer is a possible value. This assumption often leads to missing default clauses. The impact of such omissions varies, but the most common security issues arise from the use of undefined variables.

In the following sample, an undefined packet type is passed:

typedef int (*HANDLER)( unsigned char *); 

void Process(unsigned char * packet, int length) {
     unsigned char * p = packet;
     PACKET_TYPE type;
     HANDLER handler;
            
     while ((p - packet) < length) {
         type = *(PACKET_TYPE *)p;
         p += sizeof(PACKET_TYPE);
                
         switch (type) {
             case CONNECT:
                 handler = DoConnect;
                 break;
             case AUTHENTICATE:
                 handler = DoAuth;
                 break;
             case TRANSFER:
                 handler = DoXfer;
                 break;
             case DISCONNECT: 
                 handler = DoDisconnect;
                 break;
         }
                
         p += handler(p);
                
         // ...
     }
}

The result of passing an undefined packet type to the preceding code depends on whether the packet containing the undefined type is the first packet processed. If the undefined packet is the first packet, handler is undefined, resulting in a call to an undefined pointer. This situation could allow a skilled attacker to execute malicious code. If the undefined packet is not the first, handler is unchanged and the previous handler routine would be called again, potentially causing unexpected results.

To avoid the issue, add a default clause:

default:
     handler = DoInvalid;
     break;

Contiguous versus discrete

Enumerations can be either contiguous or discrete. Contiguous enumerations involve a contiguous set of integers (starting with zero by default). Discrete enumerations involve a set of distinct values that are specified individually. Contiguous enumerations can be validated effectively using range checks, while discrete enumerations can be validated more effectively using switch statements. Problems can occur when handling enumerations that contain elements of both types. The following code shows an example of a contiguous and a discrete enumeration:

enum Contiguous { a, b, c };
enum Discrete { x = 1, y = 65536, z = 0xffffffff };

The following examples illustrate some potential validation issues. In the following code sample, an enumerated type is used with groups of codes for success and failure. Positive values are used for success codes and negative ones are used for failure codes.

enum Status {
    Success,
    Connected,
    Authenticated,
    Sent,
    Received,
    Disconnected,
    Failure = 0x80000000,
    Error,
    Timeout,
    Closed,
    Pending = 0x101
};

In the following example, the status code is correctly validated. However, the Pending status is positive but not in the same range as the other success codes.

int Count[6] = {0};

bool Validate(Status s) {
     bool ok = false;

     switch (s) {
          case Success:
          case Connected:
          case Authenticated:
          case Sent:
          case Received:
          case Disconnected:
          case Failure:
          case Error:
          case Timeout:
          case Closed:
          case Pending:
              ok = true;
              break;
          default:
              ok = false;
              break;
     }
     return ok;
}

void Process(unsigned char * pkt) {
     Status status = *(Status *)pkt;

     if (!Validate(status)) {
         printf("Invalid status code - %08x\n", status);
         return;
     }

     if ((int)status > 0) {
         printf("Success - %08x\n", status);
         Count[status]++;
     } else {
         printf("Failed - %08x\n", status);
     }
}

This positive value results in an out-of-bounds array access when the count for the packet type is incremented. This situation could allow an attacker to modify data outside of the bounds of the array. The impact of this issue depends on the application, but incrementing a value in this way can have side effects that could ultimately allow an attacker to execute malicious code.

Last modified: 2013-12-21

comments powered by Disqus