Here I present a problem of memory accessing beyond valid range for an integer array pointer, the following code has two functions named good_allocation
and bad_allocation
. Glancing through the code, we can easily find the bug and correct it, the bug is at line 210; but real problems are more difficult to examine and trying to find the hidden bug only by reading millions of lines of source code is simply not practical. Our job now is to find a general method to tackle problems of this particular type:
#include
#include
#include
#include
#include
#include
#include
#define MBAD_ALLOCATED 0x1
#define MBAD_INITIALIZED 0x2
#define MBAD_MEMORY_CORRUPTED 0x3
#define MBAD_DEFAULT_LOOP 0x5
#define MBAD_DEFAULT_ARRAY 0x7
#define MENTITIES_NUM 0x2000 /* 8192 allocated chunk of entities */
static unsigned char * * gMemEntry; /* global memory allocation pointers */
static volatile unsigned int gMemState; /* memory corruption state */
static unsigned int gSizeofArray; /* default value: MBAD_DEFAULT_ARRAY */
static void bad_allocation(void) __attribute__((__noinline__));
static void good_allocation(unsigned int index, size_t memSize) __attribute__((__noinline__));
int main(int argc, char *argv[])
{
ssize_t rl1;
int urfd, idx;
unsigned int randVal, mfcnt, maxCnt;
/* initialize global variables */
gMemState = 0;
gMemEntry = nullptr;
gSizeofArray = (argc > 2) ? (unsigned int) strtoul(argv[2], nullptr, 0) : MBAD_DEFAULT_ARRAY;
if (gSizeofArray <= 1 || gSizeofArray > 4096)
gSizeofArray = MBAD_DEFAULT_ARRAY;
/* initialize local variables */
rl1 = 0;
mfcnt = 0;
idx = urfd = -1;
maxCnt = (argc > 1) ? (unsigned int) strtoul(argv[1], nullptr, 0) : MBAD_DEFAULT_LOOP;
if (maxCnt == 0 || maxCnt > 128)
maxCnt = MBAD_DEFAULT_LOOP;
maxCnt *= MENTITIES_NUM;
fprintf(stdout, "Memory allocation loop: %#x, size of array: %u\n", maxCnt, gSizeofArray);
fflush(stdout);
/* open random device */
urfd = open("/dev/urandom", O_RDONLY);
if (urfd == -1) {
fprintf(stderr, "Error, failed to open random device: %s\n", strerror(errno));
fflush(stderr);
_exit(1);
}
gMemEntry = (unsigned char * *) calloc(MENTITIES_NUM, sizeof(unsigned char *));
if (gMemEntry == nullptr) {
fputs("Error, system out of memory!\n", stderr);
fflush(stderr);
close(urfd);
_exit(2);
}
fputs("Allocating memory, stage 0, prepare...\n", stdout);
fflush(stdout);
while (mfcnt < MENTITIES_NUM) {
again:
randVal = 0;
rl1 = read(urfd, &randVal, sizeof(randVal));
if (rl1 != sizeof(randVal)) {
fprintf(stderr, "Error, failed to read random device: %s\n", strerror(errno));
fflush(stderr);
close(urfd);
_exit(3);
}
if ((randVal & (MENTITIES_NUM - 1)) == 0)
goto again;
good_allocation(randVal, (size_t) (randVal >> 24));
mfcnt++;
}
mfcnt = 0;
fputs("Allocating memory, stage 1, random memory operations...\n", stdout);
fflush(stdout);
while (mfcnt < maxCnt) {
randVal = 0;
rl1 = read(urfd, &randVal, sizeof(randVal));
if (rl1 != sizeof(randVal)) {
fprintf(stderr, "Error, failed to read random device: %s\n", strerror(errno));
fflush(stderr);
close(urfd);
_exit(4);
}
if ((randVal & (MENTITIES_NUM - 1)) == 0)
bad_allocation();
else
good_allocation(randVal, (size_t) (randVal >> 24));
mfcnt++;
}
fprintf(stdout, "Allocating memory, stage END, bug triggerred: %d\n",
gMemState >= MBAD_MEMORY_CORRUPTED);
fputs("About to free all allocated memory...\n", stdout);
fflush(stdout);
close(urfd); urfd = -1;
/* free all allocated memory */
do {
unsigned char * entry, * * memEntry;
memEntry = gMemEntry;
for (idx = 0; idx < MENTITIES_NUM; ++idx) {
entry = memEntry[idx];
memEntry[idx] = nullptr;
if (entry != nullptr) {
delete []entry;
entry = nullptr;
}
}
free(gMemEntry);
gMemEntry = nullptr;
} while (0);
fputs("********************* OK **********************\n", stdout);
fflush(stdout);
return 0;
}
void good_allocation(unsigned int index, size_t memSize)
{
unsigned char * * memEntry;
unsigned char * entry, * newEntry;
entry = newEntry = nullptr;
memEntry = gMemEntry;
index &= (MENTITIES_NUM - 1);
entry = memEntry[index];
memEntry[index] = nullptr;
memSize &= 0x0fful;
switch (memSize & 0x3) {
case 0:
memSize *= sizeof(unsigned long);
newEntry = new unsigned char [memSize + 1];
if (entry != nullptr) {
delete []entry;
entry = nullptr;
}
break;
case 1:
memSize *= sizeof(unsigned long);
newEntry = new unsigned char [memSize + 2];
if (entry != nullptr) {
delete []entry;
entry = nullptr;
}
break;
case 2:
memSize *= sizeof(unsigned long);
newEntry = new unsigned char [memSize + 3];
if (entry != nullptr) {
delete []entry;
entry = nullptr;
}
break;
case 3:
memSize *= sizeof(unsigned long);
memSize += sizeof(unsigned long);
newEntry = new unsigned char [memSize];
break;
default:
break;
}
if (entry != nullptr) {
delete []entry;
entry = nullptr;
}
memEntry[index] = newEntry;
}
void bad_allocation(void)
{
unsigned long * memPtr;
unsigned int mbadState, idx, sizeArray;
mbadState = gMemState;
if (mbadState >= MBAD_MEMORY_CORRUPTED)
return; /* Nothing TODO */
sizeArray = gSizeofArray;
memPtr = (unsigned long *) gMemEntry[0];
if (mbadState < MBAD_ALLOCATED) {
unsigned char * mptr;
mptr = new unsigned char [sizeArray * sizeof(unsigned long)];
if (mptr == nullptr) {
fputs("Error, system out of memory!\n", stderr);
fflush(stderr);
_exit(1);
}
gMemEntry[0] = (unsigned char *) mptr;
gMemState = MBAD_ALLOCATED;
return;
}
if (mbadState == MBAD_ALLOCATED) {
for (idx = 0; idx < sizeArray; ++idx)
memPtr[idx] = 0;
gMemState = MBAD_INITIALIZED;
return;
}
memPtr[sizeArray] = 0; /* Here is the bug, memory write beyond range */
gMemState = MBAD_MEMORY_CORRUPTED;
}
We need to know how the bug actually causes trouble for our small application. Compilation is important as to enable debugging information:
arm-linux-gnueabihf-g++ -std=c++11 -Wall -O1 -ggdb -march=armv7-a -marm -o access-beyond access-beyond.cpp
Next, as I’ve mentioned in another blog, we need to modify the executable file to load the correct shared libraries which also have debugging information linked in:
Now we can run the application. The results are stunning: the application random crashes and the crashing scenes vary greatly:
Then we employ the great GNU Debugger to load the application, examine the crashing application:
The bug does manifest itself in a bizarre manner: Sometimes the bug has been triggered but without causing application to crash; sometimes the bug will not trigger, and sometimes the bug causes the application to crash at very different locations. With different command-line arguments, the bug triggers but will never cause the application to crash:
Glibc has the ability to preload a shared library, to override some functions provided by other shared libraries(Details Here), via LD_PROLOAD
environment variable set to point to the path of preloaded shared library. Please note that I do not claim to have invented the method to solve problems of this type, others have long used the method; but however, I have never read others’ work. The idea is simple: replace a set of memory allocation functions provided by Glibc and set a range of memory to read-only during the allocation of a or every chunk of memory. The complete source code is listed as bellow:
#include
#include /* for mprotect(...) system call */
#ifndef NULL
#define NULL ((void *) 0ul)
#endif
#define MYMALLOC_PROTECT_TAIL 1
#define MYMALLOC_MAX_BUFSIZE 0x80000 /* 512K */
/* define 4 hook functions */
extern void free(void *);
extern void * malloc(unsigned long);
extern void * realloc(void *, unsigned long);
extern void * calloc(unsigned long, unsigned long);
/* declare functions provided by glibc */
extern void __libc_free(void *);
extern void * __libc_malloc(unsigned long);
extern void * __libc_realloc(void *, unsigned long);
/* declare memory functions */
extern void * memset(void *, int, unsigned long);
extern void * memcpy(void *, const void *, unsigned long);
#define MYMALLOC_PAGESIZE 0x1000 /* 4096 bytes */
#define MYMALLOC_MAGIC0 0x20200818 /* date when the code was written */
#define MYMALLOC_MAGIC1 0x79656a71 /* a simple string */
/* private structure for allocating & freeing memory */
struct myMalloc {
unsigned char * memBase; /* memory base pointer */
unsigned long alignedAddr; /* aligned address, protected */
unsigned long totalSize; /* total size of memory allocated, in bytes */
unsigned long reqSize; /* size of memory in bytes requested by hooked application */
unsigned int magic0; /* identifying magic value 0 */
unsigned int magic1; /* identifying magic value 1 */
unsigned char appPtr[0]; /* memory return to application */
} __attribute__((packed));
#define MYMALLOC_ALIGN 0x8
static void * my_malloc(unsigned long size, int clear)
{
struct myMalloc * mym;
unsigned char * memBase;
unsigned long mSize, totSize;
unsigned long memptr, aligned;
#if MYMALLOC_MAX_BUFSIZE > 0
/* if the size is larger than a given limit, just call __libc_malloc(...) */
if (size >= MYMALLOC_MAX_BUFSIZE)
goto noMem;
#endif
/* if size is zero, mSize should not be 0 */
mSize = size & ~(MYMALLOC_ALIGN - 0x1);
if (size & (MYMALLOC_ALIGN - 0x1))
mSize += MYMALLOC_ALIGN;
else if (mSize == 0)
mSize = MYMALLOC_ALIGN;
/* determine the total size in bytes of memory should be allocated */
totSize = mSize;
totSize += sizeof(struct myMalloc) << 0x1;
totSize += MYMALLOC_PAGESIZE * 0x2; /* or MYMALLOC_PAGESIZE * 0x3 ? */
/* allocate memory via __libc_malloc */
memBase = (unsigned char *) __libc_malloc(totSize);
if (memBase == NULL)
goto noMem;
memptr = (unsigned long) memBase;
/* find the aligned address */
#if MYMALLOC_PROTECT_TAIL
/* protect allocated memory tail from being written */
aligned = (memptr + totSize) & ~(MYMALLOC_PAGESIZE - 0x1);
aligned -= MYMALLOC_PAGESIZE;
mym = (struct myMalloc *) (aligned - mSize - sizeof(struct myMalloc));
#else
/* protect allocated memory head from being written */
aligned = memptr & ~(MYMALLOC_PAGESIZE - 0x1);
aligned += MYMALLOC_PAGESIZE;
if ((aligned - memptr) < sizeof(struct myMalloc))
aligned += MYMALLOC_PAGESIZE;
mym = (struct myMalloc *) (aligned - sizeof(struct myMalloc));
#endif
/* set allocated memory to 0x5a */
memset(memBase, 0x5A, totSize);
/* store allocation information */
mym->memBase = memBase;
mym->alignedAddr = aligned;
mym->totalSize = totSize;
mym->reqSize = size;
mym->magic0 = MYMALLOC_MAGIC0;
mym->magic1 = MYMALLOC_MAGIC1;
if (clear != 0 && size > 0)
memset(mym->appPtr, 0, size);
mprotect((void *) aligned, MYMALLOC_PAGESIZE, PROT_READ);
return (void *) mym->appPtr;
noMem:
memBase = (unsigned char *) __libc_malloc(size);
if (clear && size > 0 && memBase != NULL)
memset(memBase, 0, size);
return memBase;
}
void * malloc(unsigned long size)
{
return my_malloc(size, 0);
}
void * calloc(unsigned long nb, unsigned long size)
{
nb = nb * size;
return my_malloc(nb, -1);
}
void * realloc(void * oldPtr, unsigned long size)
{
unsigned long ptrold;
struct myMalloc * mym;
unsigned char * newPtr;
/* check for NULL pointer */
if (oldPtr == NULL)
return my_malloc(size, 0);
/* ensure that the pointer is 8-byte aligned */
ptrold = (unsigned long) oldPtr;
if (ptrold & (MYMALLOC_ALIGN - 0x1))
return __libc_realloc(oldPtr, size);
/* check for private memory allocation structure */
mym = (struct myMalloc *) (ptrold - sizeof(struct myMalloc));
if (mym->magic0 != MYMALLOC_MAGIC0 ||
mym->magic1 != MYMALLOC_MAGIC1)
return __libc_realloc(oldPtr, size); /* call `my_malloc(...) instead ? */
if (size <= mym->reqSize) {
mym->reqSize = size;
return oldPtr;
}
newPtr = (unsigned char *) my_malloc(size, 0);
if (newPtr != NULL)
memcpy(newPtr, oldPtr, mym->reqSize);
free(oldPtr);
return (void *) newPtr;
}
void free(void * freePtr)
{
unsigned long ptrAddr;
struct myMalloc * mym;
ptrAddr = (unsigned long) freePtr;
if (ptrAddr == 0)
return;
if ((ptrAddr & (MYMALLOC_ALIGN - 0x1)) != 0) {
__libc_free(freePtr);
return;
}
mym = (struct myMalloc *) (ptrAddr - sizeof(struct myMalloc));
if (mym->magic0 != MYMALLOC_MAGIC0 ||
mym->magic1 != MYMALLOC_MAGIC1) {
__libc_free(freePtr);
return;
}
mprotect((void *) mym->alignedAddr, MYMALLOC_PAGESIZE, PROT_READ | PROT_WRITE);
mym->magic0 = mym->magic1 = 0;
freePtr = (void *) mym->memBase;
__libc_free(freePtr);
}
We need to compile the above code into a shared library:
arm-linux-gnueabihf-gcc -shared -Wall -O1 -ggdb -march=armv7-a -marm -o mymalloc.so mymalloc.c
Okay, now we can be 99% positive that by simply preloading the shared library mymalloc.so
, we are able the find the bug:
To our surprise, preloading mymalloc.so
shared library causes the bug to silently go away without crashing the application! I know the reason why but it might be better left to anyone who is interested in the problem. What we should know now is that, setting a small chunk of memory to read-only will not sometimes solve our problem: it actually makes it worse. Recall that by setting the command-line arguments to 0 8
, the application will not crash despite the bug will sometimes be triggered. We can again set the arguments and see what happens:
The code runs only once and the application crashes, and the stack back-tracing brings us to the buggy function bad_allocation(...)
! What a strange debugging result! After careful calculation, we infer that pointer 0x1627000
is not writable:
And the memory map of our small application does indicate that pointer address 0x1627000
is read-only. Now as the case is closed, and with the bug found, we can next correct the bug.
Again, I do not invent the method to solve problems of this type, and the method presented here sometimes fails to help us to find the bug, accessing beyond valid range of memory. Next, we need to craft another similar but different method to solve the bug.