本節書摘來自華章計算機《編寫高品質代碼:改善c程式代碼的125個建議》一書中的第1章,建議2-6,作者:馬 偉 更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。
c99第6.2.5節的第9條規定是:涉及無符号操作數的計算永遠不會産生溢出,因為無法由最終的無符号整型表示的結果将會根據這種最終類型可以表示的最大值加1執行求模操作。也就是說,如果數值超過無符号整型資料的限定長度時就會發生回繞,即如果無符号整型變量的值超過了無符号整型的上限,就會傳回0,然後又從0開始增大;如果無符号整型變量的值低于無符号整型的下限,那麼就會到達無符号整型的上限,然後從上限開始減小。這就像一個人繞着跑道跑步一樣,繞了一圈,又傳回到出發點,是以稱為回繞。
為了加深大家對無符号整數運算産生回繞的了解,我們繼續來看代碼清單1-9所示的一個簡單例子。
在代碼清單1-9中,我們定義了3個無符号整型變量a、b與c。其中将變量a的值初始化為4294967295(即在32位機器上存儲為0xffffffff)。當程式執行語句“a+b”時,其結果超出了無符号整型的限定值(uint_max :0xffffffff),于是便産生向下回繞,是以輸出的結果為1(即0xffffffff+0x00000002=0x00000001);當程式執行語句“b-c”時,其結果為負數,于是便産生向上回繞,是以傳回的結果為4294967294(即0x00000002-0x00000004=0xfffffffe)。具體運作結果如圖1-8所示。

從代碼清單1-9中可以看出,無符号整數運算産生的回繞會給程式帶來嚴重的後果,尤其是作為數組索引、指針運算、對象的長度或大小、循環計數器與記憶體配置設定函數的實參等的時候是絕對不允許産生回繞的。是以,針對無符号整數的運算,應該采用适當的方法來防止産生回繞。例如,代碼清單1-10示範了如何簡單地處理代碼清單1-9中所産生的回繞。
在上面的代碼中,通過一些條件對無符号操作數進行測試,進而避免了無符号操作數運算産生回繞。在實際的程式設計環境中,無符号整數的回繞很可能會導緻緩沖區溢出,甚至導緻攻擊者可執行任意代碼。例如,程式繞過代碼中的大小判斷部分的邊界檢測,可以導緻緩沖區溢出,隻要使用一般的技術就能夠利用這個溢出程式。示範示例如代碼清單1-11所示。
在代碼清單1-11中,程式需要将argv[2]的内容複制到buf中,并由argv[1]指定複制的位元組數。這裡需要特别注意的語句是“if(s >= 100)”,利用該語句進行了相對嚴格的大小檢查:如果argv[1]的值大于等于buf數組的大小(100),則不進行複制。
運作代碼清單1-11,當我們執行指令“1-11 4 mawei”時,程式運作正常,并成功地複制了字元串“mawe”到buf中,運作結果如圖1-9所示。
https://yqfile.alicdn.com/552e104c3e2fcd0932a749a65af2a260b9e41af9.png" >
當我們執行指令“1-11 200 mawei”時,程式同樣運作正常,運作結果如圖1-10所示。
可當我們執行指令“1-11 65536 mawei”時,程式卻意外地繞過了大小檢查語句“if(s >= 100)”來執行相關的操作。原因很簡單,程式從指令行參數中得到一個整數值并存放在整型變量i中,然後這個值被賦予了unsigned short類型的整數s,由于s在記憶體中是用16位進行存儲的,而16位能夠存儲的最大十進制數是65535(即unsigned short存儲的範圍是0~65535),如果這個值不在unsigned short類型的存儲範圍内(0~65535),就會産生回繞。是以,當我們輸入65536時,系統将會轉換為0,進而繞過大小檢查語句“if(s >= 100)”來執行餘下的操作。可是這裡我們将buf數組的大小初始化為100,是以在執行語句“memcpy(buf, argv[2], i)”時,程式就會産生異常而導緻崩潰。其運作結果如圖1-11與圖1-12所示。
https://yqfile.alicdn.com/6fc4b345ebeb58a2c51cb73113f507006e541d12.png" >
其實,這類bug很常見,而且很容易被攻擊,這都是由于無符号整數發生回繞導緻的。由于存在回繞,當一個有符号整數被解釋成一個無符号整數時,它可能變得很大。比如,-1被當成無符号數時将會是十進制的4294967295,它是32位整數的最大值。如果我們加入的這個值被用作memcpy的參數,memcpy就會試圖複制4gb資料,很明顯這可能導緻錯誤或破壞堆棧。
除此之外,無符号整數的回繞最可能被利用的情況之一就是利用計算結果來決定将要配置設定的緩沖區的大小。通常情況下,在程式需要為一組對象配置設定記憶體空間時,會将對象的個數乘以單個對象大小,然後用所乘結果來作為參數,進而調用malloc()或calloc()函數來配置設定記憶體。這時候,隻要我們能夠控制對象的個數或單個對象的大小,就有可能讓程式配置設定錯誤大小的緩沖區。示範示例如代碼清單1-12所示。
在代碼清單1-12中,函數“int copyarray(int arr, int len)”需要将arr的内容複制到newarray中,對象的個數由len參數來指定。其中,程式使用了對象的個數乘以單個對象大小的乘積來作為malloc() 函數的參數,進而對newarray進行記憶體配置設定,即内參配置設定語句為“int newarray = (int )malloc(len*sizeof(int))”。
運作代碼清單1-12,當我們執行指令“1-12 8”時,程式運作正常,并成功地為newarray配置設定了記憶體,并将arr的内容複制到newarray中,運作結果如圖1-13所示。
https://yqfile.alicdn.com/62bb161a470a7e6efd9e701647595c6cb1f7e897.png" >
這樣看來,程式貌似沒有任何問題。但是當我們執行指令“1-12 1073741824”時,問題就出現了,抛出異常“unhandled exception at 0x004010d2 in 1-12.exe: 0xc0000005: access violation writing location 0x00387000。”。運作結果如圖1-14所示。
https://yqfile.alicdn.com/ef6c130061868559815734e313890bde3cbae5f7.png" >
是什麼原因導緻這樣的結果呢?
其實很簡單,是因為函數“int copyarray(int arr, int len)”沒有檢查參數len而導緻運算回繞失敗。在通過語句“int newarray =(int ) malloc(lensizeof(int))”給newarray配置設定記憶體時,這裡将參數設定為1073741824(十六進制是0x40000000),而“sizeof(int)”的傳回結果為4(十六進制是0x4)。當運算表達式“0x400000000x4”時,就發生了無符号整數運算回繞,所得的結果為0x0(即0x40000000*0x4=0x0)。是以,為newarray配置設定的記憶體為0。
除此之外,在通過語句“int newarray =(int ) malloc(len*sizeof(int))”給newarray配置設定記憶體時,由于參數len 的原因而造成運算回繞,是以我們可以利用它來配置設定一個任意長度的緩沖區。如上面将len參數設定為1073741824,就可能出現在沒有為newarray配置設定記憶體的情況下,卻向其中複制了數組元素,而且循環的次數還非常多,嚴重時會造成系統崩潰。當然,你還可以通過選擇合适的值賦給len參數以使得循環反複執行導緻緩沖區溢出。同時,還可以通過覆寫malloc的控制結構來執行任意惡意代碼,進而實施對堆溢出的攻擊。
在本節的最後,還需要說明的是,并不是每種運算符号都會令無符号操作數運算産生回繞,表1-5給出了可能會導緻回繞的操作符。
https://yqfile.alicdn.com/90187ea6f2e335797307546fc85eb1dc001613c5.png" >