栈攻击
- 介绍原理
- 示例代码
- 汇编分析
介绍原理
核心原理是通过 缓冲区溢出(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指向的地址;