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.
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:
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
bool objectiveRunning; // cannot overwrite any of the other
user g_User; // global variables
objective g_CurrentObjective;
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.
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.
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.
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.