- 數組越界
數組越界,是剛開始學習編程時,就不斷被別人提醒的一個點,“相當可怕”。獲取不合理數值,造成程序異常or操作計算機重要內存,造成威脅。。。原因是什么呢?數組在匯編中以棧機制實現,匯編中數組的內存的分配方式與數組越界的風險有很大關系。今天做個小實驗,來簡單探討下這個。並拓展一下,這樣的“小問題”跟匯編中的函數調用框架結合起來形成的更嚴重的問題。
- 代碼
先展示問題代碼
1 #include<stdio.h> 2 int main(){ 3 int a[3]={0,1,2}; 4 for(int i=0;i<=3;i++){ 5 a[i]=0; 6 printf("test"); 7 } 8 return 0; 9 }
諸君很容易看出,第4行for循環內的結束條件設置的顯然有問題,數組a長度位3,顯然下標只能到2,而循環中卻做了一個對所謂a[3]的賦0操作,這就是常說的數組下標越界問題。
看一下,這個問題代碼給我們帶來了什么樣的麻煩。。。
我編譯出可執行文件,運行。。。瞬間屏幕被 “test” 字符串填滿。。。
僅僅兩三秒,就不知做了多少次循環了,計算機運算就是快/xyx/xyx/xyx,展示下頁長。。。
顯然,由於這個數組越界的問題,我們陷入了死循環,(瘋狂ctrl+c,終於停了,如圖)
- 思考
那。。。為什么會死循環?
匯編語言里找問題,用gcc拿出中間匯編文件,查看匯編代碼(沒有采用什么O1/O2的優化編譯,所以以下仍含有棧幀的概念)。
1 .file "test.c" 2 .intel_syntax noprefix 3 .section .rodata 4 .LC0: 5 .string "test" 6 .text 7 .globl main 8 .type main, @function 9 main: 10 .LFB0: 11 .cfi_startproc 12 push rbp 13 .cfi_def_cfa_offset 16 14 .cfi_offset 6, -16 15 mov rbp, rsp 16 .cfi_def_cfa_register 6 17 sub rsp, 16 18 mov DWORD PTR [rbp-16], 0 19 mov DWORD PTR [rbp-12], 1 20 mov DWORD PTR [rbp-8], 2 21 mov DWORD PTR [rbp-4], 0 22 jmp .L2 23 .L3: 24 mov eax, DWORD PTR [rbp-4] 25 cdqe 26 mov DWORD PTR [rbp-16+rax*4], 0 27 mov edi, OFFSET FLAT:.LC0 28 mov eax, 0 29 call printf 30 add DWORD PTR [rbp-4], 1 31 .L2: 32 cmp DWORD PTR [rbp-4], 3 33 jle .L3 34 mov eax, 0 35 leave 36 .cfi_def_cfa 7, 8 37 ret 38 .cfi_endproc 39 .LFE0: 40 .size main, .-main 41 .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-16)" 42 .section .note.GNU-stack,"",@progbits
(雜魚懶得刪了,全貼,這是intel風格64位匯編代碼,帶諸君看)
開始分析,第11行到38行就是main函數了,.cfi_startproc和.cfi_endproc是調用框架指令,用來標記這是一段函數(這里涉及調用框架,諸君有興趣自行探索)。
12行起到16行:一系列操作據說是函數調用框架的規范步驟,成為前序,就是為了調用main函數和函數返回的正常做的工作,這里不做深究。
我們看17行,將rsp(棧頂寄存器)減了16字節,這就是為下面的數組及變量開辟空間。接着四步就可以看出一次錄入了 a[0],a[1],a[2],i 的值。接着jmp無條件跳轉到 L2段。
!!!L2里就涉及到for循環的控制了,我們開始接近問題的本源了。cmp 操作數,用來比較 [rbp-4] (我們知道這里放的是變量i) 與3的大小,接着jle(什么jump when less or equal,差不多這樣),若結果為小於等於則跳轉到L3。
L3內,上來就把我們的i的值取了出來(因為后面依據下標取數組元素要用到),接着cdqe是將32為寄存器拓展為64為寄存器rax。我們就從出問題的時間點來排查,假設這時i的值已為3(即下標已經越界了),可是到了26行時 i 的值又被賦為了0,這一步其實對應 c 文件里for循環中 a[i] = 0; 這一步,但是這里由於棧幀中內存的分配導致越界后操作到了 i 的值。可想而知,程序的邏輯是for循環到 i==4(i<=3) 時結束,而每次 i一到 3 又被我們重置為 0,for循環又如何停止???所以就死循環了唄。
(附一張main棧幀的簡圖,方便諸君理解)
沒錯,分析到這里基本就沒什么問題了,可是學習不止於此。。。
- 拓展
既然我們知道死循環是由於 i 變量被非法篡改了,導致無法滿足 i>3 的截止條件,那么我們可不可以“將錯就錯”,使 i 的值被非法篡改為滿足條件的值(比如4)
即 a[i]=0; ==> a[i]=4; 那么匯編就變為 mov DWORD PTR [rbp-16+rax*4],4 。(雖然沒啥意義,但從側面印證了,的確是 i 的值被篡改導致問題)
- 思考
幸而這里只是一個不那么緊要的變量被改,導致這個小小的程序出錯。然而更多時候,這樣的問題威脅更大:堆棧溢出!這是緩沖區溢出中危害較大的一種了,原理就是我們設計的程序並沒有對接受的數據做長度的檢查,導致該程序分配到的內存空間(棧區/緩沖區)放不下這么多內容,從而,這些數據被寫入到其他不合理的內存空間,比如上圖中返回地址,一旦被修改,下一條被執行的保不齊就是一條shellcode,系統被別人拿了特權;又或者惡意導致計算機宕機。。。(懂得不多,差不多也就了解到寫。意在引起諸君對棧溢出的關注,無論是以后走安全,還是走馬農,有良好的“意識”)
- 扯閑篇
關於CFI(函數調用框架),還是想扯點東西的,畢竟專門去了解了一堆,但是看網上人家都說現在都不用棧幀這些了,不知諸君想看不。。。