【C++】汇编角度分析栈攻击

介绍原理

核心原理是通过 缓冲区溢出(Buffer Overflow) 等漏洞,覆盖栈上的关键数据(如返回地址、函数指针),从而改变程序执行流程;

在 C++ 中,每个函数调用都会在栈上创建一个栈帧(Stack Frame),包含:

  • 局部变量:函数内定义的变量。
  • 函数参数:调用函数时传递的参数。
  • 返回地址:函数执行完后返回的地址(保存在 EIP 寄存器)。
  • 帧指针(EBP):指向当前栈帧的基址。
    栈内存是向下增长的(从高地址向低地址)

缓冲区溢出攻击
当程序向缓冲区写入数据时,若未检查输入长度,可能导致数据超出缓冲区边界,覆盖相邻的栈内存区域。攻击者可利用这一点:

  1. 覆盖返回地址为恶意代码的地址。放置攻击者指定的地址(如 shellcode 的起始地址
  2. 在栈上注入恶意代码(如 shellcode)。
  3. 触发溢出:当函数返回时,程序ret到攻击者指定的地址执行;

示例代码

下面示例代码就是通过缓冲区溢出覆盖掉栈的ret返回的地址从而改变函数返回后程序执行的地址(篡改为攻击函数地址);
大致流程:

  1. 先得到攻击函数Hack地址
  2. 调用count函数时候通过数组溢出方式,通过分析汇编代码,将汇编ret的地址修改为我们的Hack函数的地址
  3. 如此,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: 	do
    47: 	{
    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 =age1
    49: 		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 = eax
    50: 		//将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指向的地址;
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值