http://blog.csdn.net/habla/article/details/1834658
整數溢出 2007-10-20 18:39 1021人閱讀 評論(0) 收藏 舉報 table integer user 編譯器 linux核心 freebsd 整數溢出也是一種常見的軟體漏洞,由此引發的bug可能比格式化字串缺陷和緩衝區溢位缺陷更難於發現。前幾天solaris系統中就爆出被人發現了一個整數溢出漏洞。在這裡我們來翻譯phrack雜誌上的一篇關於整數溢出的文章,寫得相當詳細。平時時間不是很多,所以我不會一次把這篇文章全部翻譯後才blog出來,我會翻譯一點放一點上來還請海涵。當然由於英文水平和技術有限,翻譯中可能不能很好的把握原文的意思,如有錯誤之處,不管是英文方面的還是技術方面的都懇請您的斧正。
原文串連:http://www.phrack.org/issues.html?issue=60&id=10#article
作者:blexim@hush.com
整數溢出基礎
1: 簡介 1.1 什麼是整數。 1.2 什麼是整數溢出。 1.3 為什麼整數溢出可能會很危險。2: 整數溢出 2.1 寬度溢出 2.1.1 溢出利用 2.2 算數溢出 2.2.1 溢出利用3: 整數符號處理不當的bug 3.1 它們看起來是什麼樣子。 3.1.1 漏洞利用 3.2 整數溢出造成的符號處理錯誤4: 執行個體 4.1 整數溢出 4.2 符號處理錯誤 bugs
--[ 1.0 簡介在這篇文章中我將描述可被惡意使用者用來改變程式執行流程的兩種編程缺陷,它們不同於諸如緩衝區溢位或格式化字串那樣直接修改記憶體,而是利用使程式變數等於非預期值而改變程式執行流程。本文中的所有例子都是用c語言寫的,所以閱讀本文時您需要c語言的基礎知識。整數如何在記憶體中存放對於您理解本文會有協助但如果您不知道這方面的知識也無關緊要。----[ 1.1 什麼是整數。在計算中,整數就是一個沒有小數部分的實數。通常,整數具有同指標一樣的寬度(在32位機器中,如i386,整數是32的,在64位機器上,整數就是64位的。譯者註:在32位的x86平台c語言採用的是ILP32模型,意即I(integer),L(long), P(point)都是32位的,在AMD64平台c採用的是LP64,整數還是32位的,long和point是64位的)。有些編譯器並不會讓整數和指標具有相同的寬度,但為了討論的簡單,文中所有的例子都假定目標系統採用ILP32模型。整數和其他類型的變數一樣都僅僅是在記憶體中的一塊地區而已。當我們討論整數時我們通常用人類習慣了的十進位標記法。由於現在的電腦無法處理十進位的數,所以整數在電腦中用二進位表示。二進位只用兩個數值0和1來表示數,它並不是像十進位那樣用0-9是個數字來表示數。除了二進位和十進位標記法,同二進位之間很容易轉換的16進位也經常用來表示整數。由於經常需要表示負數,因此需要一個機制只用位元字(0和1)來表示它們。現在通用的方法是用最高位來決定一個數的符號(譯者註:cpu其實根本不知道一個數是正數還是負數,也不需要知道,需要知道的僅僅是我們的程式):如果最高位是1就把它當成負數,相反如果為0就把它當成正數(譯者註:在這裡我們並不像數學中那麼嚴謹:0既不是正數也不是負數)。這可能會有些混淆,比如在下文中將會描述的符號bug,因為並不是所有的數都帶有符號,也就是說並不是在記憶體中的所有數都用最高位來表示該數的正負,這些變數就是我們熟知的無符號數(unsigned),因此也只能賦予正數。那些既可正也可負的數我們稱之為有符號數(譯者註:原文為:whereas variables which can be either positive or negative are called unsigned。應該是筆誤。)----[ 1.2 什麼是整數溢出。由於電腦中整數都有一個寬度(本文中為32位),因此它就有一個可以表示的最大值。當我們試圖儲存一個比它可以表示的最大值還大的數時,就會發生整數溢出。ISO C99標準規定整數溢出將導致“不確定性行為”,意即遵循標準的編譯器可以做它們想做的任何事,比如完全忽略該溢出或終止進程。大多數編譯器都會忽略這種溢出,這可能會導致不確定或錯誤的值儲存在了整數變數中。----[ 1.3 為什麼整數溢出很危險。在整數溢出確實發生之前我們是無法得知它會溢出的,因此程式是沒有辦法區分先前計算出的值是否正確。如果計算結果作為一個緩衝區的大小或數組的下標時將會非常危險。當然,大多數整數溢出我們是沒有辦法利用的因為我們無法直接改寫記憶體單元,但有時整數溢出將會導致其它類型的缺陷,比如很容易發生的緩衝區溢位。整數溢出有時是很難發現的,也正因為如此,即使經過仔細審查的代碼有時候也不可避免。--[ 2.0 整數溢出當整數溢出時到底會發生什麼呢。ISO C99中這麼寫道: “當計算所涉及的運算元是無符號數時不會發生溢出,因為如果計算結果無法用無符號型整數表示 時,計算結果將會通過與不帶正負號的整數所能表示的最大值加一這個值模數運算截短。”模數運算就是我們常說的除法中的求餘。例:10 % 5 = 011 % 5 = 1[譯者:下面這段不太好表達,就原文放在這,意思還是很明了的。]so reducing a large value modulo (MAXINT + 1) can be seen as discarding theportion of the value which cannot fit into an integer and keeping the rest.In C, the modulo operator is a % sign.用一個例子來看看什麼是“不確定性行為”:有兩個32位不帶正負號的整數,a和b,我們把32位整數的最大值賦給變數a, 給b賦值1。我們把a和b的和賦值給另一個32位整數r: a = 0xffffffff b = 0x1 r = a + b現在由於a與b的和不能用32位來表示了,根據ISO標準,結果被與0x100000000模數截短了. r = (0xffffffff + 0x1) % 0x100000000 r = (0x100000000) % 0x100000000 = 0
通過模數的方法來截短可以保證只有最低的32位是有效,因此整數溢出導致計算結果被截短致該變數可以表示的寬度。這通常被稱為"wrap around",就像上例中的結果又回到了0。
----[ 2.1 寬度溢出因此整數溢出是給一個變數賦一個它不能容下的值的結果。這種溢出最簡單的類比就是給一個較小寬度的變數賦一個較大寬度的值: /* ex1.c - loss of precision */ #include <stdio.h> int main(void){ int l; short s; char c; l = 0xdeadbeef; s = l; c = l; printf("l = 0x%x (%d bits)/n", l, sizeof(l) * 8); printf("s = 0x%x (%d bits)/n", s, sizeof(s) * 8); printf("c = 0x%x (%d bits)/n", c, sizeof(c) * 8); return 0; } /* EOF */結果: nova:signed {48} ./ex1 l = 0xdeadbeef (32 bits) s = 0xffffbeef (16 bits) c = 0xffffffef (8 bits)上例中因為每一個賦值都超出了左值能夠表示的範圍,所以它們都被截斷到了左值的寬度。在這裡提一提整型提升是有必要的。當計算運算式中包括了不同寬度的運算元時,較小寬度的運算元被提升到了跟較大運算元一樣的寬度,然後再進行計算,如果計算結果儲存在較小寬度的變數中,結果會被再次截短到較小的寬度。例: int i; short s; s = i;這裡兩個運算元具有不同的寬度。因此s被提升到了32位,然後把i的值賦值給s,然後s又被截斷回16位並儲存下來。如果結果比s所能表示的最大值還大的話,它將會被截短。------[ 2.1.1 漏洞利用整數溢出不同於一般的常見的bug,它不允許直接改寫記憶體或直接控製程序流程,但它更難以捕獲。問題的根源在於程式無法在還沒有發生整數溢出之前就檢查出結果是否會發生溢出。因此,大多數整數溢出是無法利用的,儘管這樣,在某些條件下還是有可能讓一些關鍵變數包含錯誤的值,進而引起其它bug.正因整數溢出具有飄忽不定的性質,所以還是有很多情況下這種缺陷可以被利用,我不會一一列舉所有的可以利用的條件,但我會提供一些例子來展示如何利用這些漏洞,並希望能以此拋磚引玉 :) (譯者註:實踐才能出真知。我最佩服我們高中的數學老師拿來教訓我們的一句話:一看就會,一做就錯。很多人一看就說,哎呀太簡單了,可讓它真的自己來搞又不會了這是典型的浮躁。記得有位朋友在網上跟我說他對win32環境的病毒研究爐火純青了,可是我一問他對PE檔案格式吃透了沒有他居然問什麼叫PE檔案~~~狂暈。)Example 1: /* width1.c - exploiting a trivial widthness bug */ #include <stdio.h> #include <string.h> int main(int argc, char *argv[]){ unsigned short s; int i; char buf[80]; if(argc < 3){ return -1; } i = atoi(argv[1]); s = i; if(s >= 80){ /* [w1] */ printf("Oh no you don't!/n"); return -1; } printf("s = %d/n", s); memcpy(buf, argv[2], i); buf[i] = '/0'; printf("%s/n", buf); return 0; }像上面這種構造出來的代碼也許永遠不會出現在真實的代碼中,但作為一個例子他可以很好的起到示範作用。看看下面的輸入: nova:signed {100} ./width1 5 hello s = 5 hello nova:signed {101} ./width1 80 hello Oh no you don't! nova:signed {102} ./width1 65536 hello s = 0 Segmentation fault (core dumped)長度參數取自於命令列並儲存在變數i中,當把它賦值給短整型變數s時,如果值大於s能夠表示的範圍就會發生截短,(如i的值大於65535)。因此,我們是可能繞過長度限制的。這樣,標準的緩衝區溢位攻擊法就可以用來攻擊這個程式了。 ----[ 2.2 算數溢出從前面的內容可以看出,當把一個整數儲存在一個寬度不足以儲存該數時可能發生溢出。如果儲存值是由算數運算而得,程式在以後用到這個值時就會出錯,我們看看下面這個例子: /* ex2.c - an integer overflow */ #include <stdio.h> int main(void){ unsigned int num = 0xffffffff; printf("num is %d bits long/n", sizeof(num) * 8); printf("num = 0x%x/n", num); printf("num + 1 = 0x%x/n", num + 1); return 0; } /* EOF */程式運行結果: nova:signed {4} ./ex2 num is 32 bits long num = 0xffffffff num + 1 = 0x0聰明的你可能已經注意到0xffffffff就是十進位的-1,所以看起來好像我們是在做1 + (-1) = 0然而這隻是一個表象,他可能會讓我們感到糊塗,因為這些變數都是無符號的,那麼所有的運算都應該是無符號的。碰巧,很多有符號算數運算取決於整數溢出,如下例所示(假定所有運算元都是32的):-700 + 800 = 1000xfffffd44 + 0x320 = 0x100000064由於計算結果超出了變數的表示範圍,計算結果的最低32位被當成了結果而被保留了下來。上例中的最低32位就是0x64,即十進位的100。由於整數預設的是有符號的,溢出有時會導致改變正負,這將會導致一些列的非常有趣的運行行為。考慮下面這個例子: /* ex3.c - change of signedness */ #include <stdio.h> int main(void){ int l; l = 0x7fffffff; printf("l = %d (0x%x)/n", l, l); printf("l + 1 = %d (0x%x)/n", l + 1 , l + 1); return 0; } /* EOF */運行結果: nova:signed {38} ./ex3 l = 2147483647 (0x7fffffff) l + 1 = -2147483648 (0x80000000)上例中變數l被賦予了32位整型數的最大正整數值,當給它加一時結果卻變成了負數。不光算數運算可能造成溢出,任何改變該整整型變數的值的操作都可能造成溢出,如下例: /* ex4.c - various arithmetic overflows */ #include <stdio.h> int main(void){ int l, x; l = 0x40000000; printf("l = %d (0x%x)/n", l, l); x = l + 0xc0000000; printf("l + 0xc0000000 = %d (0x%x)/n", x, x); x = l * 0x4; printf("l * 0x4 = %d (0x%x)/n", x, x); x = l - 0xffffffff; printf("l - 0xffffffff = %d (0x%x)/n", x, x); return 0; } /* EOF */結果: nova:signed {55} ./ex4 l = 1073741824 (0x40000000) l + 0xc0000000 = 0 (0x0) l * 0x4 = 0 (0x0) l - 0xffffffff = 1073741825 (0x40000001)本例中的加法運算造成了溢出,它跟前一例情況一樣,緊接著下來的乘法也造成了溢出,這兩個運算其本質都是運算結果太大而一個32位的整數無法表示而造成的溢出。下面的減法稍有不同,它造成了一個下溢而非上溢:給一個整數儲存一個比它能夠表示的最小整數還小的數,也造成了迴繞。利用這種方法,我們可以強制一個加法變成減法,乘法變成除法,或則減法變成加法。------[ 2.2.1 漏洞利用算數溢出最可能被利用的情況之一是利用計算結果來決定將要分配緩衝區的大小。通常程式需要為一組對象分配記憶體空間,因此它將把對象個數乘上單個對象大小的結果作為參數調用malloc(3) 或 calloc(3)來分配記憶體。如果我們能夠控制這兩個運算元之一,我們就有可能讓程式分配錯誤大小的緩衝區,如下程式片斷所示: int myfunction(int *array, int len){ int *myarray, i; myarray = malloc(len * sizeof(int)); /* [1] */ if(myarray == NULL){ return -1; } for(i = 0; i < len; i++){ /* [2] */ myarray[i] = array[i]; } return myarray; }這個實現錯誤的函數可能因為沒有檢查參數len而失敗。在[1]處的乘法可能由於len太大而造成溢出,所以我們可以利用它來分配一個任意長度的緩衝區。通過選擇合適的len值,我們可以讓程式在[2]處的迴圈改寫超出myarray範圍的記憶體,這可以造成一個堆溢出,進而通過覆蓋malloc的控制結構來執行任意代碼,但對堆溢出的攻擊超出了本文的範圍,因此在此並不會詳細敘述如何進行堆溢出攻擊。再來一個例子: int catvars(char *buf1, char *buf2, unsigned int len1, unsigned int len2){ char mybuf[256]; if((len1 + len2) > 256){ /* [3] */ return -1; } memcpy(mybuf, buf1, len1); /* [4] */ memcpy(mybuf + len1, buf2, len2); do_some_stuff(mybuf); return 0; }本例中在[3]處對參數的檢查可以通過給這兩個參數提供合適的值從而讓計算結果溢出而繞過。如: len1 = 0x104 len2 = 0xfffffffc這兩個數相加結果是0x100(十進位256),於是在[3]處的檢查可以通過,於是調用memcpy(3)就會造成緩衝區溢位。--[ 3 符號bug無符號數被解釋成有符號數或有符號數被解釋成無符號數都可能造成符號bug。之所以會出現這種解釋錯誤,原因在於在電腦內部無符號和有符號數的儲存方法是一樣的。最近,FreeBSD和OpenBSD核心中就出現了好幾個符號處理不當的bug。----[ 3.1 符號bug的表現形式。符號bug具有大量的表現形式, 常見的有:* 有符號整數用於比較* 有符號整數用於算數運算* 不帶正負號的整數和有符號整數比較來一個經典的含有符號bug的程式: int copy_something(char *buf, int len){ char kbuf[800]; if(len > sizeof(kbuf)){ /* [1] */ return -1; } return memcpy(kbuf, buf, len); /* [2] */ }這段程式的問題在於[1]處的檢查是有符號整數的比較,而[2]出調用memcpy時參數確是無符號數,這樣通過傳遞一個負的value,就有可能通過[1]處的檢查,然後[2]處會被解釋成無符號數,於是就會造成對kbuf的溢出。再看一個例子: int table[800]; int insert_in_table(int val, int pos){ if(pos > sizeof(table) / sizeof(int)){ return -1; } table[pos] = val; return 0; }因為 table[pos] = val;等價於 *(table + (pos * sizeof(int))) = val;我們可以看出這裡的問題在於程式並沒有意識到可能有負數會參與到這個加法運算中:它主觀認為(table + pos)會比table大,所以如果給pos傳遞一個負數就會導致一些問題。------[ 3.1.1 漏洞利用這類bug易於被攻擊,原因歸結於當一個有符號數被解釋成一個無符號數時,它可能很大。比如,-1被當成無符號數時將會是十進位的(4,294,967,295),它是32位整數的最大值,加入這個值被用作memcpy的參數,memcpy就會試圖拷貝4G位元組。很明顯這可能導致斷錯誤或破壞堆棧。Sometimes it is possible to get around this problem by passing a very lowvalue for the source address and hope, but this is not always possible.----[ 3.2 整數溢出導致的符號bug有時可能可以通過溢出一個整數使其變成一個負數。由於程式可能不會意識到這種情況,所以這樣可以觸發一個符號bug.樣本 int get_two_vars(int sock, char *out, int len){ char buf1[512], buf2[512]; unsigned int size1, size2; int size; if(recv(sock, buf1, sizeof(buf1), 0) < 0){ return -1; } if(recv(sock, buf2, sizeof(buf2), 0) < 0){ return -1; } /* packet begins with length information */ memcpy(&size1, buf1, sizeof(int)); memcpy(&size2, buf2, sizeof(int)); size = size1 + size2; /* [1] */ if(size > len){ /* [2] */ return -1; } memcpy(out, buf1, size1); memcpy(out + size1, buf2, size2); return size; }這種代碼可能出現在網路守護進程當中,特別是當資料長度資訊也來自於網路時(換句話說就是來自於不可信任的用戶端)這段程式更容易被利用。在[1]處的假髮用來檢查資料長度是否會超過緩衝區的大小,但如果用戶端提供了一個合適的值就會導致加法的結果變成一個負數,比如: size1 = 0x7fffffff size2 = 0x7fffffff (0x7fffffff + 0x7fffffff = 0xfffffffe (-2)).這樣在[2]處的檢查就順利地通過了,這樣緩衝區out很可能被溢出。(事實上,(out + size1)可能被非法控制指向任意記憶體,這段程式可能被用來修改任意記憶體中的值)由整數溢出造成的這類bug利用方法與普通的符號bug一樣。--[ 4 真實代碼現實產品中有很多程式存在整數溢出和符號bug,特別是網路守護進程或作業系統核心。----[ 4.1 整數溢出下面這個例子(但無法被利用)摘自linux核心的一個安全模組,它運行在核心級: int rsbac_acl_sys_group(enum rsbac_acl_group_syscall_type_t call, union rsbac_acl_group_syscall_arg_t arg) { ... switch(call) { case ACLGS_get_group_members: if( (arg.get_group_members.maxnum <= 0) /* [A] */ || !arg.get_group_members.group ) { ... rsbac_uid_t * user_array; rsbac_time_t * ttl_array; user_array = vmalloc(sizeof(*user_array) * arg.get_group_members.maxnum); /* [B] */ if(!user_array) return -RSBAC_ENOMEM; ttl_array = vmalloc(sizeof(*ttl_array) * arg.get_group_members.maxnum); /* [C] */ if(!ttl_array) { vfree(user_array); return -RSBAC_ENOMEM; } err = rsbac_acl_get_group_members(arg.get_group_members.group, user_array, ttl_array, arg.get_group_members.max num); ... }此例中在[A]處的檢查不能有效地防範在[B]和[C]處的整數溢出。通過傳遞一個足夠大的arg.get_group_members.maxnum(大於0xffffffff / 4)將會導致[B][C]處溢出,這樣就可以強迫使ttl_array and user_array這個緩衝區比程式預期的小。由於rsbac_acl_get_group_members拷貝使用者控制的資料到這些緩衝區,所以有可能造成這些緩衝區溢位。在這個例子中,程式利用vmalloc()來分配緩衝區,所以當我們意圖溢出這些緩衝區時,只會導致一個錯誤,因此它是不可被利用的。儘管這樣,它還是提供了一個很好的例子來展示在真實的代碼中整數溢出到底是個什麼樣子。