Using structures

If you use structures in your C/C++ code, you should minimize the consequences of a buffer overflow in any fixed-width buffer or array elements within a structure (though not referenced buffers or arrays). Structures can't be rearranged by the compiler, so buffer protection mechanisms such as stack canaries aren't sufficient to ensure that a buffer overflow isn't exploitable.

Best practices

If you are using structures that contain fixed-width buffers or arrays designed to receive data that's controlled or influenced by a user, you should follow these best practices:

  • Group buffers and arrays at the end of the structure.
  • Declare local structure variables after local buffers but before any other local variables.
  • Declare global structure variables after any other global variables and before any global buffers and arrays.
  • Ensure pointers to structures do not need any special consideration.
  • Where practical, minimize the number of local variables that are cast as structures with buffers and arrays as elements. This does not apply to elements in a structure that are pointers to arrays or buffers.

To illustrate these best practices, consider the following code sample:

typedef struct objective {
     struct tm start;
     struct tm end;
     char owner[64];       // They will overflow away
     char objective[64];   // from the other elements
} objective;

If you used this structure as a local variable, then you should order the local variables as follows, with the objective declared first.

bool objective_create() {
     char exampleBuffer[20];
     objective newObjective; // success cannot be modified by an
     bool success;           // overflow in newObjective
     success = get_user_string(USER_NAME, &newObjective.owner);
     if(!success)
         return;
     success = get_user_string(OBJECTIVE_NAME, &newObjective.objective);
     if(!success)
         return;
     success = get_user_date(START_DATE, &newObjective.start);
     if(!success)
         return;
     success = get_user_date(END_DATE, &newObjective.end);
     if(!success)
         return;
     // Even if the above get_user_string() and get_user_date()
     // functions are susceptible to buffer overflows, they
     // can only modify function metadata stored on the
     // stack (such as the Saved Return Address). This should
     // be mitigated by the use of stack canaries.
     return add_objective(&newObjective);
}

Since the direction of memory layout is opposite for global variables (generally, stacks expand down, while global memory expands up), the following sample code shows how to structure your code if you want to create a global objective:

#include "user.h"
#include "objective.h"      // contains the objective related definitions

uint32_t objectivesMet;     // An overflow in g_CurrentObjective cannot
bool objectiveRunning;      // overwrite any of the other global
user g_User;                // variables
objective g_CurrentObjective;        

Structures in C/C++ and variable reordering

In C/C++, structures are aggregate types. The elements of a structure can't be reordered by the compiler. Similarly, arguments that are passed to a function can't be rearranged because this would change the type of the function.

The GCC and QNX QCC implement stack buffer protection systems to mitigate the security risk from stack-based buffer overflows. This approach involves using a variety of techniques, such as stack canaries, Address Space Layout Randomization (ASLR), using hardware features such as No eXecute (NX), and reordering the local variables within a function.

Variable reordering is used to isolate stack-based buffers as much as possible. If an overflow occurs, it will corrupt the stack canary (and maybe another stack buffer) before corrupting function arguments or other local variables on the stack. To reorder variables, allocate local stack space to all buffers directly below the stack canary, followed by other variables and copies of arguments that are output pointers.

Consider the following code sample:

bool check_domain(net_info info) {
     net_info tempInfo;
     bool status;
     char name[128];
     char domain[128];
     char answer[128];
     ...
     status = res_querydomain(&name, &domain, 
              C_IN, T_PTR, &answer, sizeof(answer));
     ...
     return status;
}

This code results in the stack frame shown in the diagram below.

Diagram showing a stack frame with a stack canary.

Although variables that have structures as their type definitions can be reordered on the stack, the elements that make up the structure can't. You must take care when using structures that contain buffers. Ideally, buffers should be collected at the end of the structure. By doing so, they'll be stored higher in memory, and can't corrupt other elements in the structure.
typedef struct _user_info {
     uint32_t cookie;
     tm lastLogon;
     uint32_t failedLogons;
     char name[256];
     char domain[256];
     char password[256];
} user_info;
This diagram illustrates how the above structure is arranged in memory.

Diagram showing a structure's layout in memory.

Placing buffers at the end of a structure protects the structure from corruption. Placing the variable that's cast as that structure at the top of the list of declared local variables (after the local stack buffers, if any) protects the rest of the local variables. However, if multiple structures with buffers on the stack are used, then the elements of some of them may still be reliably corruptible if there is a buffer overflow.

In the following code sample, any of the elements of user (such as user.cookie or user.password) can be overwritten by a buffer overflow in the character buffers in temp (temp.name, temp.domain, temp.password). This situation can occur without corrupting a stack canary.
bool AreUserDetailsCached() {
     user_info user;
     user_info temp;
     bool found = false;

     memset(temp, 0, sizeof(temp));

     while (NextUserInCache(&temp)) {
          ...
     }
     ...
     return found;
}

A buffer overflow caused by invalid credentials in the cache can allow a Denial of Service (DoS) attack by modifying the credentials of another user who is attempting to log in. This condition would be resolved only when the cache was cleared.

Last modified: 2013-12-21

comments powered by Disqus