一些Windows API導致的Crash以及使用問題總結(API的AV失敗,可以用try catch捕捉後處理)

來源:互聯網
上載者:User

標籤:轉換   fill   null   負數   ddr   行動裝置   reg   class   載入   

RegQueryValueEx

gethostbyname/getaddrinfo

_localtime64

FindFirstFile/FindNextFile

VerQueryValue

CreateFileMapping相關

SetDllDirectory

 

Windows API就沒有問題、沒有BUG嗎?答案是否定的!代碼都是寫出來,怎麼可能完全沒有問題呢?下面我們就來看看目前發現有哪些Windows API是有問題的,或者說使用上面有誤區的。

 

1、RegQueryValueEx

首先看看這個API,擷取註冊表裡面的資訊,這個API本身沒有問題,暫時還沒見到崩潰在這個API裡面的。不過這個API的使用上面有一些小技巧需要注意。使用不當會引發一些意想不到的問題甚至崩潰(API的具體使用請查閱MSDN,下面不再贅述)。

問題主要發生在我們擷取註冊表裡面的字串值的情況下,看看這樣一段代碼:

DWORD dwType = REG_SZ;

DWORD dwcbData = 0;

LONG lRet =::RegQueryValueExW(hKey, L"InstallTimeTest", NULL, &dwType, NULL, &dwcbData);

if (ERROR_SUCCESS == lRet

    && REG_SZ == dwType)

{

    PBYTE pBuffer = new BYTE[dwcbData];

我們一般在枚舉註冊表時,使用這樣的代碼來探測,某個登錄機碼需要的儲存空間是多大,然後分配空間,再進行讀取,一般情況下是沒有問題的。

但是再看看這樣的一個設定註冊表的操作:

DWORD dwType = REG_SZ;

wchar_t szInstalTime[5] = {L‘1‘, L‘2‘, L‘3‘, L‘4‘, L‘5‘};

DWORD dwcbData = sizeof(szInstalTime); // 位元組數

LONG lRet =::RegSetValueExW(hKey, L"InstallTimeTest", NULL, dwType, LPBYTE(szInstalTime), dwcbData);

if (ERROR_SUCCESS == lRet)

{

    bRet = true;

}

奇葩了吧,是的,它設定了一個字串到註冊表裡面,字串的總長度是5,位元組數是10位元組(寬字元,不包括結尾的0),傳給RegSetValueEx的長度也是10,函數執行成功了,似乎成功寫入了,但是看看註冊表裡面的情況:

竟然沒有結尾的0,此時我們再用上面的方式去讀取的時候,它首先會告訴你需要10位元組的空間,你分配10個位元組,然後再去讀就會讀到一個不以0結尾的字串存放到你的緩衝區裡面,之後你再對緩衝區進行字串的各種操作,讀寫,計算長度等等,會發生什麼我想就不用我多說了。

也許會有人說,微軟不是說了使用RegSetValueEx時,如果是設定字串的話,傳入的長度要包括結尾的0嗎?是不是RegSetValueEx用錯了,Yes,你沒說錯,但是問題是這種使用方法也是可以的,不排除有人惡意為之來使得你的程式崩潰,如果一個安全軟體輕易就這樣崩潰了……有沒有一種人生觀被徹底摧毀的敢腳。

好吧,說回來,還是看看我們應該怎樣應對這種異常情況吧,其實知道了原因我想方法也很簡單了,就是每次分配的時候,多分配一些記憶體(寬字元要多分配2個位元組,對於多行字串就要多分配4個位元組了,因為它是以兩個0結尾的)。再有一種使用方法是這樣的,如果你大概知道需要多長的空間,而且你對資料沒讀全不是很關心。那麼你提前分配一定位元組的空間(譬如N位元組),然後調用RegQueryValueExW時傳入空間大小時,傳入N-2或者N-4位元組,也可以解決這個問題。

 

2、gethostbyname/getaddrinfo

在hosts檔案裡面含有一些特殊構造的資料時,這兩個API已經被證明必然會crash,其實原因就在於它裡面有一處調用沒有對指標判空就調用wcslen來計算長度,可以通過反組譯碼mswsock模組來研究這個問題。而其解決方案也很直接,那就是直接寫一個此類API的代理函數,然後把這種crash捕獲住,發生異常時直接返回失敗即可,因為這就是一個簡單的AV異常,因此捕獲之後不會造成其它的問題,是安全的捕獲。

代理函數可以這樣寫:

struct hostent FAR * PASCAL FAR gethostbyname_safe(IN const char FAR * name)

{

    if (NULL == name)

        return NULL;

 

    __try

    {

        return ::gethostbyname(name);

    }

    __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)

    {

        return NULL;

    }

}

 

至於getaddrinfo函數則可以如法炮製。

 

3、_localtime64

這並不是一個API咯,而是微軟擴充的CRT函數,用來把一個time_t時間轉換成本地時間並存放到tm結構中。使用中發現如果給它傳入了一個不正確的值,會引發crash。最早是藍光的兄弟報告了這個函數的問題(其實這個函數和_localtime32是類似的,另外一個函數是_localtime,這個函數則是在編譯期根據是否宏定義了_USE_32BIT_TIME_T來決定是調用_localtime32還是調用_localtime64)。

你可以試試給這幾個函數傳入0之類的異常資料(負數,超大的數,64位版本超過After 23:59:59, December 31, 3000)試試……

其實微軟也知道這個問題,在MSDN上面有這樣一段話(後來想想微軟也挺坑的):

These functions validate their parameters. If timer is a null pointer, or if the timer value is negative, these functions invoke an invalid parameter handler, as described in Parameter Validation. If execution is allowed to continue, the functions return NULL and set errno to EINVAL.

它建議使用_set_invalid_parameter_handler來設定錯誤參數異常處理函數,不過很遺憾,其實這種方法也有漏洞,它雖然能捕獲這種錯誤,但是當系統濫用這種方法的時候,譬如多個模組調用這個函數來設定自己的錯誤處理函數,這種方法變得非常不可靠。譬如說,模組1調用了_set_invalid_parameter_handler,然後模組2又調用了_set_invalid_parameter_handler,過了一段時間模組2卸載了,這時錯誤處理函數還指向模組2裡面,此時如果模組1再發生參數錯誤就會導致執行一個非法地址的錯誤處理函數,問題更嚴重了,模組2卸載前把錯誤處理函數恢複如何呢?但是如果還有模組3呢?怎麼保證恢複的錯誤處理函數指標是完全可靠的呢?總的來說,此方法不太可靠,不建議使用。

更好的解決方案是按照自己的需求,對_localtime系列函數再封裝一下,先幫他們檢查一下參數,然後再傳給真正的_localtime函數處理,以規避這種問題。可以參考下面的這個函數的檢測方式(你完全可以根據自己的需求來實現這個函數)。

inline time_t FixupTime64Range(const time_t time)

{

time_t tmp_time = time;

if(tmp_time < 0 ||// underflow

tmp_time > (_MAX__TIME64_T - 14 * 60 * 60)) // overflow

{

tmp_time = 0; // reset time to 0

}

 

return tmp_time;

}

 

/* number of seconds from00:00:00, 01/01/1970 UTC to 23:59:59. 12/31/3000 UTC */

#define _MAX__TIME64_T     0x793406fffi64

14 * 60 * 60 用於控制範圍,目前最早的時區是UTC+14(http://zh.wikipedia.org/zh-hant/UTC%2B14)

 

4、FindFirstFile/FindNextFile

這兩個組合API大部分人都用過吧,用來枚舉檔案和檔案夾的,一般使用很難發現問題,但是在安全領域,用來進行檔案掃描時,因為時間一般比較長,很容易發生掃描的目標檔案夾或者磁碟裡面發生檔案被刪除的情況,此時這兩個API很容易就crash了,從實踐來看,發生在FindNextFile裡面更多一些,可以使用如下方法進行規避(封裝一個SafeFindNextFile函數,FindFirstFile也可以類似為之)。如果是比較重要的邏輯,這兩個API發生了crash之後,建議重設邏輯(譬如如果你進行中病毒掃描,可以儲存初始狀態,然後重新啟動掃描)。

BOOL bRet = FALSE;

__try

{

     bRet = FindNextFile(hFindFile, lpFindFileData);

}

__except( EXCEPTION_EXECUTE_HANDLER )

{

}

return bRet;

 

5、VerQueryValue

這個API的crash,相信肯定有不少人遇到過,這個函數通常用於從檔案的版本資源中擷取一些類似於檔案版本之類的資訊,不過這個API一般要和GetFileVersionInfo和GetFileVersionInfoSize配合調用,而且它的使用會有一些小小的麻煩(主要在於對第二個參數傳遞的字串的理解),這裡因為不是講解這個API的使用,因此就不再囉嗦了,有興趣的可以查閱MSDN上面對這個API的解說。

至於這API會發生crash的原因主要是因為它內部的一些實現上面不夠健壯,在擷取一些特殊的樣本的版本資訊時,它必然crash,說到特殊樣本,這也是為什麼一般使用的情況,基本不會發生問題,主要是安全軟體在進行樣本掃描時遇到的機率比較大。

解決方案和上面的FindNextFile類似,把這種異常catch住自己實現一個安全的函數,因為一般取檔案的版本,描述等等資訊都是輔助顯示器的,並不是關鍵邏輯,因此使用這種方式是比較簡單和安全的。

inline BOOL APIENTRY VerQueryValueWSafe(const LPVOID pBlock, LPTSTR lpSubBlock, LPVOID *lplpBuffer, PUINT puLen)

{

BOOL bRet = FALSE;

__try

{

     bRet = VerQueryValueW(pBlock, lpSubBlock, lplpBuffer, puLen);

}

__except( EXCEPTION_EXECUTE_HANDLER )

{

bRet = FALSE;

}

return bRet;

}

 

6、CreateFileMapping相關

其實CreateFileMapping這個API本身內部並沒有見過crash,其出現問題一般都是一些使用上的問題以及一些無法避免的問題導致在後面的使用中出現crash。

使用記憶體對應檔的一個常見情境是隨機讀取檔案內容時,此時會將一個檔案通過CreateFileMapping和MapViewOfFile映射到記憶體之後進行隨機讀寫。在映射完成之後,即將讀寫之前,此時如果檔案所在的硬碟卷被卸載,或者映射的是一個行動裝置(包括隨身碟,移動硬碟等等)檔案,而行動裝置被拔掉,或者一個網路檔案,網路異常斷開等等情況,會立即引發一個crash,錯誤ID類似:0xC0000006: In page error.

究其原因,在於記憶體對應檔的實現機制有點類似虛擬記憶體,他也保留了一個地址空間地區,在需要的時候才會把相關內容提交到實體儲存體器,也就是說並不是真的一開始就把整個檔案放到記憶體裡面去了,而是在需要的時候產生一個類似“缺頁中斷”響應,去外部儲存空間上面把檔案的相關內容真正映射到實體儲存體器,如果那個檔案沒了,或者隨身碟被拔掉了,那麼系統也處理不了,只好拋出異常了。

其實這個問題MSDN上面已經有個說明了:http://msdn.microsoft.com/en-us/library/windows/desktop/aa366801(v=vs.85).aspx,也給出了一種解決方案。

DWORD dwLength;

__try

{

dwLength = *((LPDWORD) lpMapAddress);

}

__except(GetExceptionCode()==EXCEPTION_IN_PAGE_ERROR ?

    EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)

{

// Failed to read from the view.

}

 

既然避免不了的問題,讓它優雅的結束也許也是一種不錯的方式。處理這個問題的另外一種方式是不使用記憶體映射,而使用ReadFile之類的API去讀取,可以通過判斷ReadFile的傳回值來識別這種情況。

 

7、SetDllDirectory

這個API的問題是因為微軟的某個版本的kernel32.dll裡面對SetDllDirectory的實現有缺陷,它內部調用的線程同步的API調錯了,造成使用這個API就會出現訪問違例的crash。後來微軟修正了kernel32.dll裡面的這個問題。但是不排除外面還存在類似的問題。

解決方案最好的一種是就是不使用這個API,從使用這個API的目的來,僅僅是為了指定動態連結程式庫的搜尋路徑,因此可以使用LoadLibraryEx來替代,這個API可指定LOAD_WITH_ALTERED_SEARCH_PATH臨時改變搜尋路徑。其實出於安全考慮,我的建議是任何時候都應該使用絕對路徑來載入DLL。

http://blog.csdn.net/magictong/article/details/11617135

一些Windows API導致的Crash以及使用問題總結(API的AV失敗,可以用try catch捕捉後處理)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.