How a buffer underflow can lead to remote code execution on 64‐bits?

In general, no, a buffer underflow cannot be used for remote code execution. Since attacker-controlled data never leaves the space allocated for it, it never has the ability to take over the program's execution flow.

A buffer underflow does have the potential for other types of attacks, such as information disclosure (if the program is counting on the buffer's original contents being wiped out by the new data).


When I originally wrote this response, it appears I glossed over some misunderstandings that you hold. These misunderstandings would likely prevent you from understanding my answer. To be clear, I'm going to make this text large to draw emphasis, not to be disrespectful:

Many of the terms you use don't mean what you seem to think they mean.

For example:

  • "the third member of memcpy" is non-existent because memcpy is a function, not a struct or union.
  • "I can’t perform a buffer overflow, since there is len" and I can't run out of petrol, since there is petrol. Am I begging the question? It seems like a fitting analogy to me, especially since theoretically speaking, your code could invoke a buffer overflow.
  • "All I can do is to set len to a value larger than the one allocated to string.st.data. This is a buffer underflow..." No, that is not the definition of a buffer underflow. A buffer underflow occurs when you access out-of-bounds an array using a negative index, or an index that would cause pointer arithmetic wrapping to occur. The latter may be possible for some configurations (even on "64-bit systems", whatever that means), but I doubt this is what you meant when you were writing these words, because you then followed them up with:
  • "... enabling me to read unallocated memory." I think perhaps you meant "uninitialised memory". To be clear, I think you meant to state that you've allocated an excess and left the excess uninitialised, and perhaps you want to do something about that (see calloc or memset).

Let us consider char *fubar = NULL; for a moment... this kind of pointer generally has a zero value. Dereferencing it is considered a null pointer dereference, but if we write something like 0["hello"] we get the same as "hello"[0] (that is 'h'). Thus, a null pointer dereference can be used in attacks when the attacker controls the expression between the square braces (as they do in your situation).

Coming back to the fubar thing; let's say we memset(&fubar, UCHAR_MAX, sizeof fubar);, now fubar is all 1-bits, meaning it might be an address that is the largest address our system could (theoretically) accommodate. What if we access fubar[1]? How can you access the element after the largest address? Does the address then wrap back around? Technically, all of this is undefined behaviour, but if I were to name that on common architecture, it would be:

  1. Arithmetic overflow of a pointer, leading into...
  2. Null pointer dereference, and/or potentially a buffer underflow.

does a buffer underflow on memcpy allow remote code execution ?

A buffer underflow may allow an attacker to overwrite function pointers located in the regions of memory before the array in question. If those function pointers are made to point at shellcode, that shellcode may be executed when they're invoked later on.

In this somewhat obscure piece of code, it appears there's a risk of buffer underflow when int has a wider domain than uint32_t, as ntohl(string.st.len)+1 would cause the uint32_t value to be converted to an int type. Consider, for example, if INT_MIN is -4294967296 (which is one less than 0 - UINT32_MAX) and INT_MAX is 4294967295... this would essentially be a 33-bit int with padding to pad out to the width of a byte; uncommon, but possible. In this circumstance, the expression ntohl(string.st.len)+1 is not uint32_t in type; it's int in type, and rather than wrapping back to 0 when unsigned integer overflow occurs, it probably wraps to -4294967296 when signed integer overflow occurs.

If you're looking for a guarantee against buffer underflow, use the U integer literal suffix (i.e. ntohl(string.st.len)+1U). Then in that situation, you'll end up with the expression being either a uint32_t or an unsigned int (depending on which type has the largest domain).

If you consider that ntohl(string.st.len) may return one less than the maximum value for that unsigned type (whatever it is), then len=ntohl(string.st.len)+1 will result in the maximum value, malloc(len+1) will cause unsigned wrapping so you'll end up invoking malloc(0) and then command[len]=0 will write well and truly beyond the end of the array. Then, of course, you'll also have a problem with memcpy(command,string.st.data,len); (this is a buffer overflow).

Buffer underflows and overflows aren't the only risk. If you don't check the return value of malloc, and malloc returns NULL then a null pointer dereference can be used to cause arbitrary code execution. This implies that you should have a method to communicate malloc failures back to the caller. It also implies that you could use that same method to check and communicate that wrapping problem back to the caller.