GDB内存调试初探四

Memory accessing beyond valid range

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 <cerrno>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

#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;
}

How the bug manifests itself

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:
同一个BUG,但应用崩溃的现象不完全相同
Then we employ the great GNU Debugger to load the application, examine the crashing application:
GDB调试工具不能直接定位缺陷
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:
应用运行的参数或环境不同时,缺陷不能复现

Replacing memory allocation functions

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 <sys/types.h>
#include <sys/mman.h> /* for mprotect(...) system call */

#ifndef NULL
#define NULL ((void *) 0ul)
#endif
#define MYMALLOC_PROTECT_TAIL  1
#define MYMALLOC_MIN_BUFSIZE   0       /* 0K */
#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 long             magic0;        /* identifying magic value 0 */
    unsigned long             magic1;        /* identifying magic value 1 */
    unsigned char             appPtr[0];     /* memory return to application */
} __attribute__((packed));

#define MYMALLOC_ALIGN        (sizeof(char *) * 0x2)
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_MIN_BUFSIZE > 0
    if (size <= MYMALLOC_MIN_BUFSIZE)
        goto noMem;
#endif
#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);
    if (aligned != memptr)
        aligned += MYMALLOC_PAGESIZE;
    mym = (struct myMalloc *) (aligned + MYMALLOC_PAGESIZE - 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      = mSize;
    mym->magic0       = MYMALLOC_MAGIC0;
    mym->magic1       = MYMALLOC_MAGIC1;

    if (clear != 0 && size > 0)
        memset(mym->appPtr, 0, mSize);
    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) {
        /* bugfix of writing read-only memory */
        /* mym->reqSize = size; */
        return oldPtr;
    }
    newPtr = (unsigned char *) my_malloc(size, 0);
    if (newPtr == NULL)
        return 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:
预加载了mymalloc.so之后,缺陷不再复现
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:
预加载mymalloc.so并设置应用的参数为 0 8
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:
查看0x1627000内存属性
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.

Conclusion

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.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值