pwn从0-0.5系列(1):pwn入门基础(栈篇) – 作者:D1stiny

常见寄存器

32位:

4个数据(通用)寄存器:(eax、ebx、ecx、edx) 6个段寄存器:(ES、CS、SS、DS、FS、GS) 2个变址寄存器:(ESI、EDI) 2个指针寄存器(ESP、EBP) 1个指令指针寄存器:EIP

32位CPU有4个32位通用寄存器:eax、ebx、ecx、edx。

段寄存器

段寄存器:在8086系统中,访问存储器的地址码由段地址和段内偏移地址两部分组成。段寄存器用来存放各分段的逻辑基值,并指示当前正在使用的4个逻辑段,包括代码段寄存器CS、堆栈段寄存器SS、数据段寄存器DS和附加段数据寄存器ES。

32位CPU有6个段寄存器,分别如下:

CS:代码段寄存器 ES:附加段寄存器

DS:数据段寄存器 FS:附加段寄存器

SS:堆栈段寄存器 GS:附加段寄存器

在16位CPU系统中,只有4个段寄存器(CS、DS、ES、SS)

变址寄存器

32位CPU有2个32位通用寄存器ESI和EDI,其低16位对应8086CPU中的SI和DI,对低16位数据的存取,不影响高16位的数据。
esi、edi、si和di称为变址寄存器,它们主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器。

指针寄存器

32位CPU有2个32位通用寄存器EBP和ESP,其低16位对应先前CPU中的BP和SP,对低16位数据的存取,不影响高16位的数据。
EBP、ESP、BP和SP称为指针寄存器,主要用于存放堆栈内存储单元的偏移量,用它们可实现多种存储器。

ebp为基指针寄存器,用它可直接存取堆栈中的数据。

esp为堆栈指针寄存器,用它只可访问栈顶。

指令指针寄存器

32位CPU把指令指针扩展到32位,并记作EIP,EIP的低16位与先前CPU中的IP作用相同。
指令指针EIP、IP是存放下次将要执行的指令在代码段的偏移地址。

64位:

X86-64有16个64位寄存器,分别是:%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。

%rax 作为函数返回值使用。

%rsp 栈指针寄存器,指向栈顶

%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数

%rbx,%rbp,%r12,%r13,%r14,%r15 用作数据存储,调用子函数之前要备份它,以防被修改

%r10,%r11 数据存储,使用之前要先保存原值

寄存器%eax、%edx和%ecx为主调函数保存寄存器(caller-saved registers),当函数调用时,若主调函数希望保持这些寄存器的值,则必须在调用前显式地将其保存在栈中;被调函数可以覆盖这些寄存器,而不会破坏主调函数所需的数据。寄存器%ebx、%esi和%edi为被调函数保存寄存器(callee-saved registers),即被调函数在覆盖这些寄存器的值时,必须先将寄存器原值压入栈中保存起来,并在函数返回前从栈中恢复其原值,因为主调函数可能也在使用这些寄存器。此外,被调函数必须保持寄存器%ebp和%esp,并在函数返回后将其恢复到调用前的值,即必须恢复主调函数的栈帧。

汇编指令

mov

mov %esp,%ebp

将主调函数的栈顶指针%esp赋给被调函数帧基指针%ebp。此时,%ebp指向被调函数新栈帧的起始地址(栈底),亦即旧%ebp入栈后的栈顶

push & pop

相关寄存器: esp, ebp

相关操作: pop, push

//建立被调用者函数的堆栈框架
push %ebp
mov %esp, %ebp

//拆除框架
mov %ebp, %esp
pop %ebp
ret

push:压栈

push %eax

栈顶指针ESP减小4个字节;以字节为单位将寄存器数据(四字节,不足补零)压入堆栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4指向的地址单元

相当于:

sub $4, %esp//栈顶指针减4
mov %eax, %esp//%eax -> esp 地址

pop:出栈

pop %eax

栈顶指针ESP指向的栈中数据被取回到寄存器;栈顶指针ESP增加4个字节

mov (%esp), %eax
add %4, %esp//栈顶指针加4

call&ret

call 0x111

将当前的指令指针EIP(该指针指向紧接在call指令后的下条指令)压入堆栈,以备返回时能恢复执行下条指令;然后设置EIP指向被调函数代码开始处,以跳转到被调函数的入口地址执行

push %eip
mov $0x111, %eip//当前地址压栈,存入新地址

ret

与call指令配合,用于从函数或过程返回。从栈顶弹出返回地址(之前call指令保存的下条指令地址)到EIP寄存器中,程序转到该地址处继续执行(此时ESP指向进入函数时的第一个参数)。使用该指令前,应使当前栈顶指针所指向位置的内容正好是先前call指令保存的返回地址。

pop %eip

enter&leave

enter

push %ebp
mov %esp, %ebp//将堆栈置空(栈上重堆)

leave

恢复主调函数的栈帧以准备返回。等价于指令序列movl %ebp, %esp(恢复原ESP值,指向被调函数栈帧开始处)和popl %ebp(恢复原ebp的值,即主调函数帧基指针)

mov %ebp, %esp
pop %ebp//将堆栈置空(撤销堆栈)

sub

sub %esp

将栈顶指针%esp减去指定字节数(栈顶下移),即为被调函数局部变量开辟栈空间。为立即数且通常为16的整数倍(可能大于局部变量字节总数而稍显浪费,但gcc采用该规则保证数据的严格对齐以有效运用各种优化编译技术)

栈介绍

栈是一种典型的先进后出( First in Last Out )的数据结构,其操作主要有压栈(push)与出栈(pop)两种操作,如下图所示。两种操作都操作栈顶,当然,它也有栈底。
Data_stack.png

每个程序在运行时都有虚拟地址空间,其中某一部分就是该程序对应的栈,用于保存函数调用信息和局部变量。此外,常见的操作也是压栈与出栈。

Snipaste_2021-03-07_20-34-55.png

函数调用栈

程序的执行过程可看作连续的函数调用。当一个函数执行完毕时,程序要回到调用指令的下一条指令(紧接call指令)处继续执行。函数调用过程通常使用堆栈实现,每个用户态进程对应一个调用栈结构(call stack)。编译器使用堆栈传递函数参数、保存返回地址、临时保存寄存器原有值(即函数调用的上下文)以备恢复以及存储本地局部变量。

栈增长方向:高地址->低地址

调用方式 cdecl stdcall fastcall
参数传递 从右到左压栈 从右到左压栈 左边两个参数 分别放在ECX 和EDX寄存器, 其余的参数从 右到左压栈
栈清理 调用者 函数自身 函数自身

栈帧

函数调用经常是嵌套的,堆栈中会有多个函数的信息。每个未完成运行的函数均占用一个独立的连续区域,称作栈帧。栈帧是堆栈的逻辑片段,当调用函数时栈帧被压入堆栈, 当函数返回时栈帧被从堆栈中弹出。

栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。编译器利用栈帧,使得我们可以清楚函数参数和函数中局部变量的分配与释放。编译器将控制权移交函数本身之前,插入特定代码将函数参数压入栈帧中,并分配足够的内存空间用于存放函数中的局部变量。

栈帧对函数的每次递归调用,都会分配给该函数一个新的栈帧,这样就巧妙地隔离当前调用与上次调用。栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。

函数调用

在调用一个函数时,系统会为这个函数分配一个栈帧,栈帧空间为该函数所独有。

调用者调用一个函数的过程大致如下:

函数参数从右到左入栈,返回地址入栈,上一函数ebp入栈

在上一函数ebp入栈后,就开辟了被调函数的新栈帧,接下来便是被调函数临时变量入栈等操作,如果被调函数里有继续调用新函数的操作,将继续开始上述的一系列操作,不断循环嵌套下去。下图表示函数调用过程中栈的布局情况。

Snipaste_2021-03-07_20-36-55.png

函数调用结束时的变化,主要就是按相反的顺序将数据弹出栈:

弹出临时变量,弹出调用函数的ebp值,存到ebp寄存器中,弹出返回地址,存到eip寄存器中

返回地址是用call指令调用函数时下一条指令的地址,存到eip中,程序继续执行下一条指令。

函数调用约定

创建一个栈帧的最重要步骤是主调函数如何向栈中传递函数参数。主调函数必须精确存储这些参数,以便被调函数能够访问到它们。函数通过选择特定的调用约定,来表明其希望以特定方式接收参数。此外,当被调函数完成任务后,调用约定规定先前入栈的参数由主调函数还是被调函数负责清除,以保证程序的栈顶指针完整性。

函数参数的传递顺序和方式:

最常见的参数传递方式是通过堆栈传递。主调函数将参数压入栈中,被调函数以相对于帧基指针的正偏移量来访问栈中的参数。对于有多个参数的函数,调用约定需规定主调函数将参数压栈的顺序(从左至右还是从右至左)。某些调用约定允许使用寄存器传参以提高性能。

栈的维护方式:

主调函数将参数压栈后调用被调函数体,返回时需将被压栈的参数全部弹出,以便将栈恢复到调用前的状态。该清栈过程可由主调函数负责完成,也可由被调函数负责完成。

Name-mangling策略:

又称函数名修饰(Decorated Name)规则。编译器在链接时为区分不同函数,对函数名作不同修饰。若函数之间的调用约定不匹配,可能会产生堆栈异常或链接错误等问题。因此,为了保证程序能正确执行,所有的函数调用均遵守一致的调用约定。

一个想入门pwn的垃圾ctfer,哪里不对或理解错误还请大佬们指点。
这篇文章也在本地积累了一段时间,某些内容具体来源也记不清了,还请大佬们海涵

来源:freebuf.com 2021-03-07 20:56:30 by: D1stiny

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论