Android 栈溢出攻击—[0]原理

时间:2022-10-23 15:32:46

栈,是一种数据结构,是一个先进后出的数据表,所以会为其抽象出栈底和栈顶两个属性。简单的理解,可以用生活中的装书的箱子类比,我们取书的顺序和放书的顺序相反,即第一本放入的书最后才能取出。对栈的操作也非常简单:压栈和弹栈。如图,

Android 栈溢出攻击—[0]原理

通常,在谈论栈溢出时,所指的栈是操作系统虚拟地址空间中的栈区stack:

Android 栈溢出攻击—[0]原理

系统栈由系统自动维护(我们可以在编译器编译时指定大小),用于实现函数调用,下面来看下协同栈是如何协助实现函数调用。

Function call,What has happened

我们通过实际的代码调用过程,来看下操作系统如何配合系统栈完成函数的调用过程。先看下代码:

int func_B(int arg_B1 , int arg_B2)
{
int var_B;
var_B = arg_B1 - arg_B2;
return var_B;
}

int func_A(int arg_A1 , int arg_A2)
{
int var_A;
var_A = func_B(arg_A1 + arg_A2) + arg_A2;
return var_A;
}

int main(int argc , char **argv , char **envp)
{
int var_main;
var_main = func_A(4 , 3);
}

熟悉程序编译、链接和加载过程的同学,应该了解程序编译和加载的过程:程序会被编译生成可执行文件(elf或so),当可执行文件启动时,操作系统会为其创建一个进程,进程首先会为程序创建独立的虚拟地址空间,然后读取可执行文件头,建立虚拟地址空间和可执行文件的映射,最后让CPU从可执行文件入口开始执行(更详细的过程可以看潘大大的《程序员的自我修养 – 链接、装载与库》)。

这里主要看下可执行文件的代码段(二进制形式)加载到程序中的样子:
Android 栈溢出攻击—[0]原理

编译后的二进制代码以函数为单位散乱的分布在进程的虚拟内存的代码区。再来看下当CPU执行程序的流程:

Android 栈溢出攻击—[0]原理

注:main函数并不是可执行文件的入口函数,真正的入口函数由运行库(Android下是bionic)实现,用来对程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造等等。最后由入口函数调用main函数,开始执行程序主体。

从CPU取指执行过程中,我们发现一个重要的问题,当子函数调用执行完毕后,CPU可以回到母函数调用子函数指令的后续指令继续执行,这是怎么实现的?其实这就是通过系统栈来实现的。

首先,我们来看下一个函数的调用过程:

  1. 参数入栈:将参数从右向左依次压入栈中(这里压栈的顺序根据不同的函数调用约定有所改变)。

  2. 返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。

  3. 代码区跳转:CPU从当前代码区跳转到被调用函数的入口处。

  4. 栈帧调整:

    1. 保存当前栈帧状态值,以备后面恢复本栈帧时使用(EBP入栈)。

    2. 将当前栈帧切换到新栈帧(将ESP值装入EBP,更新栈帧底部)。

    3. 在新栈帧中为函数分配空间(将ESP减去所需空间大小,通过抬高栈顶的方式分配空间)。

整个过程如图,

Android 栈溢出攻击—[0]原理

同样,函数返回的步骤如下:

  1. 保存返回值:通常是将函数返回值保存在寄存器EAX中。

  2. 弹出当前栈帧,恢复上一个栈帧环境

    1. 在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧空间。

    2. 将当前栈帧底部保存的前栈帧EBP值弹入EBP中,恢复上一个栈帧。

    3. 将函数返回地址弹给EIP寄存器。

  3. 跳转:按照函数返回地址跳回母函数中继续执行。

整个过程如图,

Android 栈溢出攻击—[0]原理

通过上面对函数调用过程,看到了系统栈在函数调用过程中扮演了很重要作用。

缓冲区溢出

缓冲区溢出,简单的讲,就是在一个大的缓冲区的数据向小缓冲区复制的过程中,没有注意小缓冲区的边界,“撑爆”了较小的缓冲区,从而冲掉了和小缓冲区相邻的内存区域的其他数据而引起的内存问题。

Android 栈溢出攻击—[0]原理

缓冲区溢出后会产生很多异常结果,例如,程序诡异的bug、程序崩溃、系统panic等等。

而缓冲区溢出攻击就是利用缓冲区溢出后破环了某些内存区,使得目标程序跳转到攻击者所控制的内存,来执行攻击者放入其中的邪恶代码。攻击的方式很多(网上大把的资料),但攻击的缓冲区溢出位置主要集中两处内存位置:栈和堆栈。

栈溢出攻击

介绍完系统栈、数调用实现的过程以及缓冲区溢出知识后,来看下栈中的缓冲区溢出和攻击过程。

首先,栈溢出的缓冲区指的是存在于被调用函数的栈帧中的局部变量区,常见的是函数中的局部数组变量。比如下面的函数:

void func_A(char * str)
{
char buf_A[6];
strcpy(buf_A , str);
}

当函数func_A被调用时,它的栈帧如图,

Android 栈溢出攻击—[0]原理

从这个栈帧中我们得到两个结论:

  • 系统栈的内存地址增长方向是从高地址到低地址。

  • 栈中的局部数组变量的数据增长方向是从低地址向高地址。

正是由于缓冲区的“生长”方向和栈的“生长”方向相反,使得缓冲区溢出后有机会覆盖函数返回地址,从而劫持了程序流程,执行防止在缓冲区中的邪恶代码(shellcode),

Android 栈溢出攻击—[0]原理

小结

这里只描述了栈缓冲区溢出攻击最基础的方式—攻击函数返回地址,执行缓冲区中的邪恶代码。但实际中根据目标系统平台的不同攻击方式更加多样,并随着各种漏洞缓解技术(XN、ALSR等)开启利用方式趋于复杂。但不变的是其本质:

  • 利用漏洞控制CPU的PC指针劫持病态的程序流程。

  • 获取目标进程地址空间布局,定位并执行布局在特定内存位置(堆,堆栈等)上的邪恶代码。