1. 首页
  2. 技术小贴

Pwn入坑之栈基础(1)

内存四区

技术小贴
技术小贴

代码区

.text

这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指令执行。

数据区

.data

也叫静态区(static area),用于存储全局变量静态变量常量,程序结束后由系统释放。

分为初始化的全局变量、静态变量、常量和未初始化的全局变量、静态变量、常量。

堆区

通过mallocfreenewdelete等函数动态地分配和回收内存,进程可以在堆区动态地请求一定大小的内存,并在用完后归还给堆区。

地址由高到低生长

栈区

存放局部变量函数参数返回数据返回地址,函数调用结束时释放。

栈区用以保护函数现场,可以动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行。

地址由低到高生长

内存区段选讲

一般情况下,一个程序本质上都是由 bss段、data段、text段三个段组成。

程序编译完成之后,已初始化的全局变量、静态变量保存在.data 段中,未初始化的全局变量、静态变量保存在.bss 段中。

举个例子:

//程序1
int a[30000];
void main()
{
    ...
}
//程序2
int a[30000]={1,2,3,4,5,6};
void main()
{
    ...
}

不难发现,编译之后的程序2明显大于程序1,为什么?

程序1位于bss段,而程序2位于data段。

全局的未初始化变量存在于bss段中,具体体现为一个占位符,全局的已初始化变量存于data段中,而函数内的局部变量都在栈上分配空间。

bss段并不给该段的数据分配空间,只是记录数据所需空间的大小,

而data段需要为数据分配空间,数据保存在目标文件中,包含经过初始化的全局变量以及它们的值。

bss段紧跟在data段的后面,包含data和bss段的整个区段此时通常称为数据区。

Pwn入坑之栈基础(1)

BSS段溢出攻击(选)

题目:game_of_chance.c

链接:BSS段溢出攻击

栈的结构

通常我们说的栈,是一种数据结构,数据存储方式为先进后出,压栈(push)和出栈(pop) 。

而我们这里说的栈,每个程序都有自己的进程地址空间,进程地址空间中的某一部分就是该程序的栈,用于保存函数调用信息和局部变量。

程序的栈是从进程空间的高地址向低地址增长的,数据是从低地址向高地址存放的。

Pwn入坑之栈基础(1)

函数调用

调用流程

函数调用大致包括以下几个步骤:

  1. 参数入栈:将参数从右向左依次压入系统栈中。
  2. 返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继
    续执行。
  3. 代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。
  4. 栈帧调整:具体包括以下方法

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

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

    给新栈帧分配空间(把 ESP 减去所需空间的大小,抬高栈顶)

返回流程

  1. 保存返回值:通常将函数的返回值保存在寄存器 EAX 中。
  2. 弹出当前栈帧,恢复上一个栈帧。

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

    将当前栈帧底部保存的前栈帧 EBP 值弹入 EBP 寄存器,恢复出上一个栈帧。

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

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

源码分析

//referer to <<0day security vulnerability analyze technology>>
intfunc_B(int arg_B1, int arg_B2)
{
int var_B1, var_B2;
var_B1=arg_B1+arg_B2;
var_B2=arg_B1-arg_B2;
return var_B1*var_B2;
}
intfunc_A(int arg_A1, int arg_A2)
{
int var_A;
var_A = func_B(arg_A1,arg_A2) + arg_A1 ;
return var_A;
}
int main(int argc, char **argv, char **envp)
{
int var_main;
var_main=func_A(4,3);
    return var_main;
}

案例流程

当 CPU 在执行调用func_A 函数的时候,会从代码区中 main函数对应的机器指令的区域
跳转到func_A函数对应的机器指令区域,在那里取指并执行;

func_A函数执行完闭,需要
返回的时候,又会跳回到main函数对应的指令区域,紧接着调用 func_A 后面的指令继续执行
main 函数的代码。

在这个过程中,CPU 的取指轨迹如下图所示:

Pwn入坑之栈基础(1)

那么 CPU 是怎么知道要去func_A 的代码区取指,在执行完func_A后又是怎么知道跳回
到 main 函数(而不是func_B的代码区)的呢?

当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并把它压入栈中。

这个栈帧中的内存空间被它所属的函数独占,正常情况下是不会和别的函数共享的。当函数返回时,系统栈会弹出该函数所对应的栈帧。

调用约定

声明 参数入栈顺序 恢复栈平衡的位置
__cdecl 右→左 母函数
__fastcall 右→左 子函数
__stdcall 右→左 子函数

如果要明确使用某一种调用约定,只需要在函数前加上调用约定的声明即可,否则默认情
况下,VC 会使用__stdcall 的调用方式。

栈帧结构

栈帧概要

每一个函数独占自己的栈帧空间。当前正在运行的函数的栈帧总是在栈顶。Win32 系统提
供两个特殊的寄存器用于标识位于系统栈顶端的栈帧。

  1. ESP:栈指针寄存器(extended stack po inter),其内存放着一个指针,该指针永远指向
    系统栈最上面一个栈帧的栈顶。
  2. EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向
    系统栈最上面一个栈帧的底部。

Pwn入坑之栈基础(1)

ESP 和 EBP 之间的内存空间为当前栈帧,EBP 标识了当前栈帧的底部,ESP
标识了当前栈帧的顶部。

除了与栈相关的寄存器外,还有一个至关重要的寄存器EIP。
EIP:指令寄存器(Extended Instruction Pointer),其内存放着一个指针,该指针永远指向下
一条等待执行的指令地址。

可以说如果控制了 EIP 寄存器的内容,就控制了进程——我们让 EIP 指向哪里,CPU 就会
去执行哪里的指令。

Pwn入坑之栈基础(1)

内含信息

在函数栈帧中,一般包含以下几类重要信息。

  1. 局部变量:为函数局部变量开辟的内存空间。
  2. 栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部
    可以通过堆栈平衡计算得到),用于在本帧被弹出后恢复出上一个栈帧。
  3. 函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,
    以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。

案例分析

以上一小节的函数调用的源代码做函数栈帧级别的分析,具体如下:

  1. 在 main 函数调用 func_A 的时候,首先在自己的栈帧中压入函数返回地址,然后为
    func_A 创建新栈帧并压入系统栈。
  2. 在 func_A 调用 func_B 的时候,同样先在自己的栈帧中压入函数返回地址,然后为
    func_B 创建新栈帧并压入系统栈。
  3. 在 func_B 返回时,func_B 的栈帧被弹出系统栈,func_A 栈帧中的返回地址被“露”
    在栈顶,此时处理器按照这个返回地址重新跳到 func_A 代码区中执行。
  4. 在 func_A 返回时,func_A 的栈帧被弹出系统栈,main 函数栈帧中的返回地址被“露”
    在栈顶,此时处理器按照这个返回地址跳到 main 函数代码区中执行。

Pwn入坑之栈基础(1)

参数传递

32位程序

  • 通过栈传参
  • 从右向左传入参数
  • 先压入最后一个参数

64位程序

  • 使用rdi rsi rdx rcx r8 r9 依次接收先传入的六个参数
  • 剩下传入的参数和32位的传参方式相同

原创文章,作者:小嵘源码,如若转载,请注明出处:https://www.lcpttec.com/pwnbasic/

联系我们

176-888-72082

在线咨询:点击这里给我发消息

邮件:2668888288@qq.com

工作时间:周一至周五,9:00-18:00,节假日休息

QR code