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.

Recommendations

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 recommendations:

  • 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.
  • 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 recommendations, 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), if you need to create a global objective, then the source should look as follows:

#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 do this, after a stack canary has been placed in the stack frame of a function, local stack space is allocated with all buffers located directly below the canary, followed by all 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 writing C/C++ that uses structures containing buffers. Ideally, buffers should be collected at the end of the structure. By doing so, they'll be stored higher in memory than the other elements of the structure and so can't corrupt the other elements.

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 itself, and 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;
}

Although this is just an example, if there were invalid credentials in the cache that caused a buffer overflow, this could cause 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.