从汇编代码理解数组越界访问漏洞

  • A+
所属分类:笔记

数组越界访问漏洞是 C/C++ 语言中常见的缺陷,它发生在程序尝试访问数组元素时未正确验证索引是否在有效范围内。通常情况下,数组的索引从0开始,到数组长度减1结束。如果程序尝试访问小于0或大于等于数组长度的索引位置,就会导致数组越界访问。由于 C/C++ 没有对数组做边界检查,不检查下标是否越界可以提升程序运行的效率,导致在程序编译过程中它并不定会造成编译错误。攻击者可以利用数组越界访问漏洞来读取或修改程序内存中的数据,甚至执行恶意代码。这种漏洞可能会导致程序崩溃、拒绝服务,或者泄露敏感信息。因此,在开发软件时,应该确保对数组访问进行边界检查,以防止发生这种类型的漏洞。

一、漏洞代码示例

首先展示一个包含数组越界访问漏洞的代码:

#include<stdio.h>
int main(){
    int a[7]={0,1,2,3,4,5,6};
    for(int i=0;i<=7;i++)
    {
        a[i]=0;
        printf("This is a test!\n");
    }
    return 0;
}

可以很容易看出,数组的长度为7,下标只能取到6,而在代码的第4行for循环的终止条件却为 i<=7,导致在第6行会执行一个"a[7]=0"的赋值操作,出现数组越界问题。

我们编译并执行上述代码,发现居然成功通过编译,没有任何报错!但是陷入了死循环,并输出了满屏的"This is a test!"。

从汇编代码理解数组越界访问漏洞

上述代码中的数组越界访问导致程序陷入死循环,那么为什么会出现这种现象呢?

二、汇编代码分析

我们通过 Compiler Explorer 将上述程序处理为 intel 风格的x86_64汇编代码看看:

从汇编代码理解数组越界访问漏洞

下面我们来分析上述汇编代码。

  • 4-6行:常规的栈帧初始化,这里不做赘述。
  • 7-14行:栈顶指针(rsp)减32字节,为数组及变量开辟空间,接着分别赋入 a[0] - a[6] 以及变量 i 的值。
  • 15行:跳转至L2段。
  • 24-25行:比较(cmp)指针[rbp-4]处存放的值(变量i的值)与7的大小,如果小于或等于则跳转到L3段(jle,jump when less or equal)。
  • 17行:取出指针指向[rbp-4]位置的值,存入寄存器eax中。
  • 18行:执行cdqe指令,将32位寄存器eax扩展为64位寄存器rax。
  • 19行:到了导致数组越界的关键地方,在这里将0赋值给了[rbp-32+rax*4],我们假设某个时刻rax(存放变量i的寄存器)为7(即下标已经越界),此时,[rbp-32+rax*4] = [rbp-4] = i = 0(如14行)。也就是说,每当i=7时,在源代码第6行a[i]=0处会再次将0赋值给i,就导致for循环中 i=7 -> i=0,最终产生死循环。

小结一下,导致死循环的根本原因是数组赋值语句a[i]=0非法篡改了i的值。假设这里我们将a[i]=0更改为a[i]=7,此时[rbp-32+rax*4]= [rbp-4] = i = 7,程序依然可以正常执行。

从汇编代码理解数组越界访问漏洞

三、进一步分析

这里我们更改一下示例代码为:

#include<stdio.h>
void func1()
{
    long a[2]={0,1};
    a[3] = 0;
    a[4] = 0;
}

void func2()
{
    printf("func2 excuted!");
}

int main(){
    func1();
    printf("No error!");
    return 0;
}

执行上述代码,会出现异常:

从汇编代码理解数组越界访问漏洞

因为在函数func1栈的高地址位存放了其返回地址,当该函数执行完后,cpu依据这个地址信息回到main函数中继续执行后续代码,而第5行a[3]=0处,将数值0覆盖了该返回地址,因此导致程序异常。根据上述逻辑,如果我们将a[3]指向函数func1的返回地址,理论上程序应该也可以正常执行。

同样地,通过Compiler Explorer得到该程序的汇编代码:

从汇编代码理解数组越界访问漏洞

将a[3]指向函数func1的返回地址,即修改 a[3]=0 为 a[3]=0x401171,再次执行程序,可以正常得到输出结果:

从汇编代码理解数组越界访问漏洞

另外,我们还可以做如下修改:

  • a[3]=0x401151;
  • a[4]=0x401171;

将数组a指向函数func2的初始化地址,让func1调用并执行func2函数。执行程序,得到如下输出结果:(成功调用了函数func2)。

从汇编代码理解数组越界访问漏洞

四、总结

根据上述示例可以发现,通过数组越界来覆盖函数的返回地址可以更改程序的执行流程。同样地,攻击者可以填写存放恶意代码的地址,从而引导函数func1去执行该恶意代码。因此,在编写代码中对数组做边界检查尤为重要。

参考:

[1] [汇编杂项]关于_高级语言中 数组越界与汇编中 栈溢出的_联系的思考 - SachieW - 博客园 (cnblogs.com)

[2] 通过查看汇编理解数组越界 - 知乎 (zhihu.com)

[3] CPU眼里的:数组越界 | 堆栈溢出_哔哩哔哩_bilibili

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: