堆栈基础(一)

本文稿费80软妹币

砸个广告:各位在网络安全方面有新创作的小伙伴,快将你们的心得砸过来吧~

文章以word形式发至邮箱:

minwei.wang@dbappsecurity.com.cn

有偿投稿,记得留下你的姓名联系方式哦~

-START-

新手入门pwn之栈溢出系列,先学习堆栈的基础,函数调用栈这些。

运行时栈

运行时栈(runtime stack)是有cpu内部硬件直接支持的,也是实现过程调用和过程返回机制的基本组成部分。在大多数时我们称运行时栈为:堆栈

这里的堆栈和数据结构里的栈抽象数据类型是不同的,堆栈即运行时栈在系统层上(由硬件直接实现) 处理子过程调用;堆栈抽象数据类型通常用于实现依赖后进先出操作的算法,一般使用高级语言如c++/java等编写。

栈方向

https://www.zhihu.com/question/36103513

https://www.cnblogs.com/xkfz007/archive/2012/06/22/2558935.html

栈方向跟体系结构有关系,x86是向下增长,x86硬件直接支持的栈确实是“向下增长”的,由高地址向低地址增长:push指令导致sp自减一个slot,pop指令导致sp自增一个slot。其它硬件有其它硬件的情况。arm没有固定,但一般操作系统会选择向下增长

在内存管理中,与栈对应是堆。对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方式是向下的,是向着内存地址减小的方向增长。在内存中,“堆”和“栈”共用全部的自由空间,只不过各自的起始地址和增长方向不同,它们之间并没有一个固定的界限,如果在运行时,“堆”和 “栈”增长到发生了相互覆盖时,称为“栈堆冲突”,系统肯定垮台。

三个寄存器

函数状态主要涉及三个寄存器

rsp/esp/sp:栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶 rbp/ebp/bp:基址指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部函数栈帧 rip/eip/ip:指令寄存器, 其内存放着一个指针,该指针永远指向下一条待执行的指令地址。

push / pop操作

运行时栈是有cpu直接管理的内存数组, 它使用连个寄存器,ss和esp(32是esp,16位是sp,64位是rsp), ss寄存器存放的段地址,esp是堆栈指针寄存器,指向最后压入到堆栈上的数据。我们很少直接操纵esp的值,而是由call,ret,push和pop指令间接修改的。

堆栈段的操作步骤——

压栈(入栈)push sth-> [esp]=sth,esp=esp-4

弹栈(出栈)pop sth-> sth=[esp],esp=esp+4

http://bestwing.me/2017/03/18/stack-overflow-one/

函数调用方式和栈帧

参考:https://www.zhihu.com/question/22444939

如下代码:

#include<stdio.h> void func_A(int arg_A1, int arg_A2);void func_B(int arg_B1, int arg_B2,int arg_B3);int main(int argc, char *argv[], char **envp){ printf("main start"); int arg_A1 = 1; int arg_A2 = 2; func_A(arg_A1, arg_A2); }void func_A(int arg_A1, int arg_A2){ int arg_A3 = 3; func_B(arg_A1,arg_A2,arg_A3); }void func_B(int arg_B1, int arg_B2,int arg_B3){ printf("%d,%d,%d\n",arg_B1,arg_B2,arg_B3); }

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

参数入栈:将参数从右向左依次压入系统栈中 返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行 代码区跳转:处理器从当前代码区跳转到被调用函数的入口处 栈帧调整:具体包括保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈) 将当前栈帧切换到新栈帧。(将ESP值装入EBP,更新栈帧底部) 局部变量入栈,esp减小。 恢复的过程就是现弹出栈帧(pop ebp),这样就可以恢复出调用函数的栈帧了,此时栈顶会指向返回地址,再将返回地址弹出(pop eip),并保存到eip中,之后就会回到原来调用函数的下一条地址继续运行了。

栈帧的调整过程:

在main函数调用func_A的时候,首先在自己的栈帧中压入函数返回地址;

然后为func_A创建新栈帧并压入系统栈,在func_A调用func_B的时候,同样先在自己的栈帧中压入函数返回地址;

然后为func_B创建新栈帧并压入系统栈,在func_B返回时,func_B的栈帧被弹出系统栈,func_A栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址重新跳到func_A代码区中执行在func_A返回时,func_A的栈帧被弹出系统栈,main函数栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址跳到main函数代码区中执行在实际运行中;

main函数并不是第一个被调用的函数,程序被装入内存前还有一些其他操作,上图只是栈在函数调用过程中所起作用的示意图。

函数调用图如下:

栈帧调整图如下:

函数调用时栈内的数据从高地址到低地址分别是函数参数入栈(从右到左),返回地址入栈,ebp入栈,esp分配填充地址, 局部变量mov入栈。

函数调用的汇编代码为:

main函数

push ebp //保留旧栈帧

mov ebp,esp //分配新的栈帧 sub esp,4C //设置填充空间大小为4C push ebx //寄存器入栈 push esi push edi lea edi,dword ptr ss:[ebp-4C] 将栈顶指针地址给edi mov ecx,13 //计数器设置为13 mov eax,CCCCCCCC //填充数据 rep stosd dword ptr es:[edi],eax //重复填充13次0xccccccc到[ebp-4C]~[ebp]到这个空间 mov dword ptr ss:[ebp-4],pwn3.42201C //pwn3.c:6, [ebp-4]:"test programmer", 42201C:"test programmer" //局部变量入栈(并不是push的) mov dword ptr ss:[ebp-8],1 //pwn3.c:7 局部变量入栈 mov dword ptr ss:[ebp-C],2 //pwn3.c:8 局部变量入栈 mov eax,dword ptr ss:[ebp-C] //pwn3.c:9 push eax //右边第一个参数2入栈 mov ecx,dword ptr ss:[ebp-8] push ecx //右边第二个参数1入栈 call <pwn3.ILT+5(_func_A)> //函数调用

参数入栈的方式:取决于调用约定,一般情况下:

X86 从右向左入栈,X64 优先寄存器,参数过多时才入栈

main函数的栈帧:

ebp:0019ff40

我们看到函数调用后首先是函数参数入栈,函数调用后会发生什么呢?我们来看一看:

push ebp 保存main函数的栈帧ebp mov ebp,esp //设置funcA的栈帧ebp sub esp,44 //给fucA栈帧分配填充字节空间44 push ebx //寄存器入栈 push esi push edi lea edi,dword ptr ss:[ebp-44] mov ecx,11 mov eax,CCCCCCCC rep stosd dword ptr es:[edi],eax //填充同上 mov dword ptr ss:[ebp-4],3 //局部变量mov入栈 mov eax,dword ptr ss:[ebp-4] push eax //函数参数push入栈 mov ecx,dword ptr ss:[ebp+C] push ecx mov edx,dword ptr ss:[ebp+8] push edx edx:&"ALLUSERSPROFILE=C:\\ProgramData" call <pwn3.ILT+10(_func_B)> //调用fucB add esp,C pop edi pwn3.c:14 pop esi pop ebx add esp,44 cmp ebp,esp call <pwn3._chkesp> mov esp,ebp pop ebp ret

func_A的栈帧: ebp=0019FED8

func_B的汇编代码:

push ebp mov ebp,esp sub esp,40 push ebx push esi push edi lea edi,dword ptr ss:[ebp-40] mov ecx,10 mov eax,CCCCCCCC rep stosd dword ptr es:[edi],eax mov eax,dword ptr ss:[ebp+10] push eax //函数参数入栈 mov ecx,dword ptr ss:[ebp+C] push ecx mov edx,dword ptr ss:[ebp+8] push edx push pwn3.422030 422030:"%d,%d,%d\n" call <pwn3.printf> add esp,10 pop edi pop esi pop ebx add esp,40 cmp ebp,esp call <pwn3._chkesp> mov esp,ebp ret

将这段代码的所有汇编一步一步跟踪了解清楚了后,对堆栈算是大概了解了,下面就是入门栈溢出了,之后学到栈溢出再来更新。

-END-

原文发布于微信公众号 - 安恒网络空间安全讲武堂(cyberslab)

原文发表时间:2018-06-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏三丰SanFeng

Linux64位程序移植

1 概述 Linux下的程序大多充当服务器的角色,在这种情况下,随着负载量和功能的增加,服务器所使用内存必然也随之增加,然而32位系统固有的4GB虚拟地址空间限...

2397
来自专栏技术记录

通讯协议序列化解读(一) Protobuf详解教程

前言:说到JSON可能大家很熟悉,是目前应用最广泛的一种序列化格式,它使用起来简单方便,而且拥有超高的可读性。但是在越来越多的应用场景里,JSON冗长的缺点导致...

1001
来自专栏向治洪

23种设计模式

一、设计模式的分类 总体来说设计模式分为三大类: 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。 结构型模式,共七种:...

4336
来自专栏十月梦想

ES6数据传递的传值和传址

看一下上面一段代码,通过正常的理解确实这个样子,但是下面的代码我们只改变了test.y值而obj的也随之改变!这个样子是用于前一部分是传值,后面是传地址!   ...

1964
来自专栏龙首琴剑庐

Java动态代理一览笔录

1、什么是代理? 比较经典的含义如销售代理,签订合同的基础上,为委托人(厂商)销售某些特定产品或全部产品的代理商,对价格、条款及其他交易条件可全权处理。我们从销...

2966
来自专栏用户2442861的专栏

C内存管理一 概述

我们写了这么多年的程序员,可能理论方面还比不上大学生。有人 "嘘"我了,如果有能回答以下几个问题的同学请举手: 1.面试经常遇到:同学请说说堆栈的区别? 2....

761
来自专栏hbbliyong

C++为啥要使用new

1.为什么要有new? 为什么要有new?为什么要动态创建对象?为什么有时候不用new,有时候又用new,比如: // Cocos2d-x3.x的Value类,...

38312
来自专栏Java面试通关手册

深入理解原型模式 ——通过复制生成实例

Java面试通关手册(Java学习指南,欢迎Star,会一直完善下去,欢迎建议和指导):https://github.com/Snailclimb/Java_G...

1793
来自专栏算法channel

面试被问到动态内存分配时需要注意哪些坑,该怎么回答?

面试时,面试官问我们Java,Python这种语言那是必须要准确回答的,很多系统如果对性能要求高的话,底层一般会用到C/C++语言,因此被问到底层语言的相关知识...

1313
来自专栏王清培的专栏

.NET项目开发—浅谈面向对象的纵横向关系、多态入口,单元测试(项目小结)

阅读目录: 1.开篇介绍 2.使用委托消除函数串联调用 2.1.使用委托工厂转换两个独立层面的对象 3.多态入口(面向对象继承体系是可被扩展的) 4.多态的受...

19210

扫码关注云+社区