棧攻擊
- 介紹原理
- 示例代碼
- 匯編分析
介紹原理
核心原理是通過 緩沖區溢出(Buffer Overflow) 等漏洞,覆蓋棧上的關鍵數據(如返回地址、函數指針),從而改變程序執行流程;
在 C++ 中,每個函數調用都會在棧上創建一個棧幀(Stack Frame),包含:
- 局部變量:函數內定義的變量。
- 函數參數:調用函數時傳遞的參數。
- 返回地址:函數執行完后返回的地址(保存在 EIP 寄存器)。
- 幀指針(EBP):指向當前棧幀的基址。
棧內存是向下增長的(從高地址向低地址)
緩沖區溢出攻擊
當程序向緩沖區寫入數據時,若未檢查輸入長度,可能導致數據超出緩沖區邊界,覆蓋相鄰的棧內存區域。攻擊者可利用這一點:
- 覆蓋返回地址為惡意代碼的地址。放置攻擊者指定的地址(如 shellcode 的起始地址
- 在棧上注入惡意代碼(如 shellcode)。
- 觸發溢出:當函數返回時,程序ret到攻擊者指定的地址執行;
示例代碼
下面示例代碼就是通過緩沖區溢出覆蓋掉棧的ret返回的地址從而改變函數返回后程序執行的地址(篡改為攻擊函數地址);
大致流程:
- 先得到攻擊函數Hack地址
- 調用count函數時候通過數組溢出方式,通過分析匯編代碼,將匯編ret的地址修改為我們的Hack函數的地址
- 如此,count函數返回后程序就會沿著我們修改的Hack方向運行;
#include <iostream>
#include <iomanip>void Hack()
{unsigned long long x = 0;for (int i = 0; true; i++){if (i % 100000000 == 0){system("cls");std::cout << "\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n";std::cout << "\n 你的系統已經被我們拿下! hacked by 黑兔檔案局:[ID:000001 ]\n";std::cout << "\n\\>正在傳輸硬盤數據....已經傳輸" << x++ << "個文件......\n\n";std::cout << std::setfill('>')<< std::setw(x % 60) << "\n";std::cout << "\n\\>攝像頭已啟動!<==============\n\n";std::cout << std::setfill('#') << std::setw(x % 60) << "\n";std::cout << "\n\\>數據傳輸完成后將啟動自毀程序!CPU將會溫度提升到200攝氏度\n";std::cout << "\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n";}}
}int GetAge()
{int rt;std::cout << "請輸入學員的年齡:";std::cin >> rt;return rt;
}int count()
{int i{};int total{};int age[10]{};do{age[i] = GetAge();total += age[i];//將AGE[I]保存到數據庫中} while (age[i++]);return total;
}int main()
{std::cout << "======= 驢百萬學院 學員總年齡統計計算系統 =====\n";std::cout << "\n API:"<<Hack<<std::endl;std::cout << "\n[說明:最多輸入10個學員的信息,當輸入0時代表輸入結束]\n\n";std::cout << "\n驢百萬學院的學員總年齡為:" << count();
}
匯編分析
直接從count函數匯編入手:
int count()42: {
00061200 push ebp //當前函數調用前的基址指針(Base Pointer,通常用來指向當前函數的棧幀)壓入棧中,以便后續在函數結束時能夠恢復到調用前的狀態。
00061201 mov ebp,esp //將當前棧指針(Stack Pointer,指向當前棧頂)的值賦給基址指針 ebp,這樣 ebp 現在指向當前函數count的棧幀
00061203 sub esp,34h //esp=esp-52,這條指令將棧指針 esp 減去 0x34h(52 的十進制值),這樣就為當前函數的棧幀分配了 52 字節的空間。在函數執行過程中,局部變量和其他數據將會存儲在這段空間中。43: int i{};
00061206 mov dword ptr [i],0 // 0移動到變量 i 所在的內存位置。dword ptr 來指示操作數(內存里的數據)的大小為雙字(32 位,4字節),這是因為 i 是一個整數類型變量。44: int total{};
0006120D mov dword ptr [total],0 45: int age[10]{};
00061214 xor eax,eax //將寄存器 eax 與自身進行異或操作,結果存儲回 eax。這個操作的目的是將 eax 清零,因為在這段代碼中,eax 被用來存儲數組 age 的起始地址。
00061216 mov dword ptr [age],eax
00061219 mov dword ptr [ebp-30h],eax //48字節偏移量
0006121C mov dword ptr [ebp-2Ch],eax //44
0006121F mov dword ptr [ebp-28h],eax
00061222 mov dword ptr [ebp-24h],eax
00061225 mov dword ptr [ebp-20h],eax
00061228 mov dword ptr [ebp-1Ch],eax
0006122B mov dword ptr [ebp-18h],eax
0006122E mov dword ptr [ebp-14h],eax
00061231 mov dword ptr [ebp-10h],eax //16
棧底ebp存的就是count函數的下一條地址,我們目的是修改這一個地址;
這段代碼對應函數棧如下:
46: do47: {48: age[i] = GetAge();
00061234 call GetAge (0611D0h) //調用了一個函數 GetAge,并將返回值存儲在寄存器 eax 中。
00061239 mov ecx,dword ptr [i] //將變量 i 的值加載到寄存器 ecx 中 ecx=i =0 ecx=i=1
0006123C mov dword ptr age[ecx*4],eax //此時將寄存器 eax 中的年齡age存儲到 age[ecx*4] 中,age[ecx*4]將age數組的內存地址偏移ecx*4個字節。此時ecx*4 是因為 age 是一個數組,每個元素占據 4 個字節。 age[0] = eax =age0 age[1] = eax =age149: total += age[i];
00061240 mov edx,dword ptr [i] //將變量 i 的值加載到寄存器 edx 中 edx =i= 0 edx =i= 1
00061243 mov eax,dword ptr [total] // 將變量 total 的值加載到寄存器 eax 中 eax = total
00061246 add eax,dword ptr age[edx*4] //[]里的理解為字節的位置,而不是元素,將 age[i] 的值加到 total 中 eax = eax+age[0]+age[1]
0006124A mov dword ptr [total],eax //將寄存器 eax 中的值存儲回變量 total 中。 total = eax50: //將AGE[I]保存到數據庫中51: } while (age[i++]); //就是讓i++并且判斷是否這次輸入的age[i]==0
0006124D mov ecx,dword ptr [i] //將變量 i 的值加載到寄存器 ecx 中 ecx=i=0
00061250 mov edx,dword ptr age[ecx*4] //V將 age[i] 的值加載到寄存器 edx 中 edx = age[0]//edx值放在[ebp-0Ch]這篇內存應該是專門為了與0比較開辟的內存
00061254 mov dword ptr [ebp-0Ch],edx //將寄存器 edx 中的值存儲到內存中的位置 [ebp-0Ch]。ebp-12
00061257 mov eax,dword ptr [i] //將變量 i 的值加載到寄存器 eax 中 eax = i =0
0006125A add eax,1 //將寄存器 eax 中的值加 1 eax = eax+1
0006125D mov dword ptr [i],eax //將寄存器 eax 中的值存儲回變量 i 中 i = eax
00061260 cmp dword ptr [ebp-0Ch],0 //將內存中的位置 [ebp-0Ch]ebp-12 的值與 0 比較 age[0]與0比較
00061264 jne count+34h (061234h) //如果不相等,則跳轉到 count+34h(52) 處執行 //查詢 count 0x00061200h +34h后是0x00061234。跳到了call GetAge處52: return total;
00061266 mov eax,dword ptr [total] // total 變量的值加載到寄存器 eax 中53: }
//函數清尾
00061269 mov esp,ebp
0006126B pop ebp //在pop ebp指令中,ebp是一個操作數,指示將棧頂元素彈出并將其存儲到ebp寄存器中
0006126C ret //此時已經返回了[total]算出了正確total,運行結束!
注意:下面這張圖的代碼age數組是改為5的,對應下面圖片,最后輸入的就是Hack的API地址,此時替換為了原來ret指向的地址;