分析內存越界一個方法

內存越界是我們軟件開發中經常遇到的一個問題。不經意間的複製常常導致很嚴重的後果。經常使用memset、memmove、strcpy、strncpy、strcat、sprintf的朋友肯定對此印象深刻,下面就是我個人在開發中實際遇到的一個開發問題,頗具典型。


#define MAX_SET_STR_LENGTH 50
#define MAX_GET_STR_LENGTH 100

int* process(char* pMem, int size)
{
char localMemory[MAX_SET_STR_LENGTH] = {0};
int* pData = NULL;

/* code process */
memset(localMemory, 1, MAX_GET_STR_LENGTH);
memmove(pMem, localMemory, MAX_GET_STR_LENGTH);
return pData;
}

這段代碼看上去沒有什麼問題。我們本意是對localMemory進行賦值,然後拷貝到pMem指向的內存中去。其實問題就出在這一句memset的大小。根據localMemory初始化定義語句,我們可以看出localMemory其實最初的申明大小隻有MAX_SET_STR_LENGTH,但是我們賦值的時候,卻設置成了MAX_GET_STR_LENGTH。之所以會犯這樣的錯誤,主要是因為MAX_GET_STR_LENGTH和MAX_SET_STR_LENGTH極其相似。這段代碼編譯後,產生的後果是非常嚴重的,不斷沖垮了堆棧信息,還把返回的int*設置成了非法值。

那麼有沒有什麼好的辦法來處理這樣一個問題?我們可以換一個方向來看。首先我們查看,在軟件中存在的數據類型主要有哪些?無非就是全局數據、堆數據、棧臨時數據。搞清楚了需要控制的數據之後,我們應該怎麼對這些數據進行監控呢,一個簡單有效的辦法就是把memset這些函數替換成我們自己的函數,在這些函數中我們嚴格對指針的複製、拷貝進行判斷和監督。

(1)事實上,一般來說malloc的數據是不需要我們監督的,因為內存分配的時候,通常庫函數會比我們要求的size多分配幾個字節,這樣在free的時候就可以判斷內存的開頭和結尾處有沒有指針溢出。朋友們可以試一下下面這段代碼。


void heap_memory_leak()
{
char* pMem = (char*)malloc(100);
pMem[-1] = 100;
pMem[100] = 100;
free(pMem);
}

pMem[-1] = 100是堆左溢出, pMem[100]是堆右溢出。

(2)堆全局數據和棧臨時數據進行處理時,我們利用memset初始化記錄全局指針或者是堆棧臨時指針

a) 首先對memset處理,添加下面一句宏語句

#define memset(param, value, size) MEMORY_SET_PROCESS(__FUNCTION__, __LINE__, param, value, size)

b) 定義內存節點結構


typedef struct _MEMORY_NODE
{
char functionName[64];
int line;
void* pAddress;

int size;
struct _MEMORY_NODE* next;

}MEMORY_NODE;

其中functionName記錄了函數名稱,line記錄文件行數, pAddress記錄了指針地址, size指向了pAddress指向的內存大小,next指向下一個結構節點。

c)記錄內存節點屬性

在MEMORY_SET_PROCESS處理過程中,不僅需要調用memset函數,還需要對當前內存節點進行記錄和保存。可以通過使用單鏈表節點的方法進行記錄。但是如果發現pAddress指向的內存是malloc時候分配過的,此時就不需要記錄了,因為堆內存指針溢出的問題lib庫已經幫我們解決了。

d)改造原有內存指針操作函數

比如對memmove等函數進行改造,不失去一般性,我們就以memmove作為範例。

添加宏語句 #define memmove(dst, src, size) MEMMOVE_PROCESS(dst, src, size)


void MEMMOVE_PROCESS(void* dst, const void* src, int size)
{
MEMORY_NODE* pMemNode = check_node_exist(dst);
if(NULL == pMemNode) return;

assert(dst >= (pMemNode->pAddress));
assert(((char*)dst + size) <= ((char*)pMemNode->pAddress + pMemNode->size));

memmove(dst, src, size);
return;
}


e)下面就是內存節點的刪除工作。

我們知道函數是需要反覆使用堆棧的。不同時間相同的堆棧地址對應的是完全不同的指針內容,這就要求我們在函數返回的時候對內存地址進行清理,把內存節點從對應的鏈表刪除。

我們知道在函數運行後,ebp和esp之間的內存就是通常意義上臨時變量的生存空間,所以下面的一段宏就可以記錄函數的內存空間。


#ifdef MEMORY_LEAK_TEST
#define FUNCTION_LOCAL_SPACE_RECORD()\
{\
int* functionBpRecord = 0;\
int* functionSpRecord = 0;\
}
#else
#define FUNCTION_LOCAL_SPACE_RECORD()
#endif

#ifdef MEMORY_LEAK_TEST
#define FUNCTION_LEAVE_PROCESS()\
{\
__asm { mov functionBpRecord, bp\
mov functionSpRecord, sp}\
FREE_MEMORY_NODE(functionBpRecord, functionSpRecord)\
}
#else

#define FUNCTION_LEAVE_PROCESS()
#endif

這兩段宏代碼,需要插在函數的起始位置和結束的位置,這樣在函數結束的時候就可以根據ebp和esp刪除堆棧空間中的所有內存,方便了堆棧的重複使用。如果是全局內存,因為函數的變化不會導致地址的變化,所以沒有必要進行全局內存節點的處理。

內存溢出檢查流程總結:

(1)對memset進行重新設計,記錄除了malloc指針外的一切內存;

(2)對memmove, strcpy, strncpy,strcat,sprintf等全部函數進行重新設計,因為我們需要對他們的指針運行範圍進行判斷;

(3)在函數的開頭和結尾位置添加宏處理。函數運行返回前進行節點清除。


分享到:


相關文章: