这是一套Linux Pwn入门教程系列,作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的一些题目和文章整理出一份相对完整的Linux Pwn教程。

课程回顾>>Linux Pwn入门教程第一章:环境配置

更多Pwn视频课程:https://www.ichunqiu.com/courses/pwn?from=weixin

本系列教程仅针对i386/amd64下的Linux Pwn常见的Pwn手法,如栈,堆,整数溢出,格式化字符串,条件竞争等进行介绍,所有环境都会封装在Docker镜像当中,并提供调试用的教学程序,来自历年赛事的原题和带有注释的python脚本。

教程中的题目和脚本若有使用不妥之处,欢迎各位大佬批评指正。

今天是Linux Pwn入门教程第二章:栈溢出基础,阅读用时约10分钟。

函数的进入与返回

要想理解栈溢出,首先必须理解在汇编层面上的函数进入与返回。首先我们用一个简单执行一次回显输入的程序hello开始。用IDA加载hello,定位到main函数后我们发现这个程序的逻辑十分简单,调用函数hello获取输入,然后输出“hello,”加上输入的名字后退出。使用F5看反汇编后的C代码可以非常方便的看懂逻辑。

我们选中IDA-View窗口或者按Tab键切回到汇编窗口,在main函数的call hello一行下断点,开启32位的Docker环境,启动调试服务器后直接按F9进行调试。

如图,这是当前IDA的界面。在这张图中我们需要重点注意到的东西有栈窗口,EIP寄存器,EBP寄存器和ESP寄存器。

首先我们可以看到EIP寄存器始终指向下一条将要执行的指令,也就是说如果我们可以通过某种方式修改EIP寄存器的值,我们就可以控制整个程序的执行,从而“pwn”掉程序(要验证这一点,我们可以在EIP后面的数字上点击右键选择Modify value.......把数值改成080484DE然后F9继续执行,从而跳过call hello一行)。

剩下的东西都和栈相关。顾名思义,栈就是一个数据结构中的栈结构,遵循先入后出的规则。这个栈的最小单位是函数栈帧,一个函数栈帧的结构如图所示:

栈的生长方式是向低地址生长,也就是说这张图的方向和IDA中栈窗口的方向是一样的,越往上地址值越小。同样的,新入栈的栈帧在IDA的窗口中会把原来的栈帧“压”在下面。

ESP和EBP两个寄存器负责标定当前栈帧的范围。图中标黑的部分即为实际上ESP和EBP中间的最大区域(为了方便讲解,我们把EIP和参数也列入一个函数的函数栈帧)。

图中的局部变量和参数很好理解,但EBP和EIP又是什么意思呢?我们回到IDA调试窗口。按照程序的逻辑,接下来应该是执行call hello这行指令调用hello这个函数,函数执行完后回到下一行的mov eax, 0,其地址为080484DE.然后我们再把当前ESP和EBP的值记下来(受地址空间随机化ASLR的影响,每台电脑每次运行到此处的ESP和EBP值不一定相同),然后按F7进入hello函数。

如图,执行完call hello这一行指令后发生了如下改变。由此我们可以得知call指令是可以改变EIP“始终指向下一条指令地址”的行为的,且call指令会把call下一条指令地址压栈。我们可以理解为call hello等价于push eip; mov eip, [hello]。所以我们的第一个问题“栈帧中的EIP是什么意思”的回答就是:栈帧中的EIP是call指令的下一条指令的地址,我们继续F8单步执行。

如图,通过依次执行三条指令,程序为hello函数开辟了新的栈帧,同时把原来的栈帧,即执行了call hello函数的main函数的栈帧的栈底EBP保存到栈中。继续往下执行到read函数,然后随便输入一些比较有标志性的内容,比如12345678,我们就会发现存储输入的局部变量buf就在这片新开辟的栈帧中。

我们已经接触到了栈帧的开辟与被使用情况,接下来我们再通过调试继续学习栈帧的销毁。继续F8到leave一行,此时我们会发现栈帧再次回到了刚执行完sub esp, 18h的状态。

执行完leave一行指令后栈帧被销毁,整体状态回到了call hello执行前的状态。即leave指令相当于add esp, xxh; mov esp, ebp; pop ebp

再次F8,发现EIP指向了call hello的下一行指令,同时栈中保存的EIP值被弹出,栈顶地址+4. 即retn等同于pop eip

此时hello函数代码执行完毕,控制流程返回到了调用hello函数的main函数中。

栈溢出实战

通过上一节的调试,我们大概理解了函数栈的初始化和销毁过程。我们发现随着我们的输入变多,输入的内容离栈上保存的EIP地址越来越近,那么我们可不可以通过输入修改掉栈上的EIP地址,从而在retn指令执行完后“pwn”掉程序呢?我们按Ctrl+F2结束掉当前的调试,再试一次。为了节约时间,这回我们直接把断点下在hello函数里的call _read一行。

启动调试,程序中断后界面如下:

通过观察read函数的参数和栈中的保存的EIP地址,我们计算出两者的偏移是0x16个字节,也就是说输入0x16=22个字节的数据,我们的输入就会和栈中的EIP“接上”,输入22+4=26个字节,我们的输入就会覆盖掉EIP。那么我们构造payload为‘A’*22+‘B’*4

即AAAAAAAAAAAAAAAAAAAAAABBBB,根据我们的推测,在EIP寄存器指向retn指令所在地址时,栈顶应该是‘BBBB’。即retn执行完之后,EIP里的值将不再是图中框起来的080484DE,而是42424242(BBBB的ASCII值),按F8使IDA挂起,在docker环境中输入payload:

栈中的EIP果然按照我们的推测被修改成42424242了。显然,这是一个非法的内存地址,它所在的内存页此时对我们来说并没有访问权限,所以我们运行完retn后程序将会报错。

选择OK,继续F8并且选择将错误传递给系统,这个进程接收到信号后将会结束,调试结束。我们通过一个程序本身的bug构造了一个特殊输入结束掉了它。

结合pwntools打造一个远程代码执行漏洞exp

通过上一节的内容,我们已经可以做到远程使一个程序崩溃。不要小看这个成果。如果我们能挖掘到安全软件或者系统的漏洞从而使其崩溃,我们就可以让某些保护失效,从而使后面的入侵更加轻松。当然,我们也不应该满足于这个成果,如果可以继续扩大这个漏洞的利用面,制造一个著名的RCE(远程代码执行),为所欲为,岂不是更好?

当然,CTF中的绝大部分pwn题也同样需要通过暴露给玩家的一个IP地址和端口号的组合,通过对端口上运行的程序进行挖掘,使用挖掘到的漏洞使程序执行不该执行的代码,从而获取到flag,这也是我们学习的目标。

为了降低难度,我在编写hello这个小程序的时候已经预先埋了一个后门——位于0804846B的名为getShell的函数。

如图,这个函数唯一的作用就是调用system("/bin/sh")打开一个bash shell,从而可以执行shell命令与系统本身进行交互。

正常的程序流程并不会调用这个函数,所以我们将会利用上一节中发现的漏洞劫持程序执行流程,从而执行getShell函数。

首先我们把hello的IO转发到10001端口上。

然后我们从Docker环境中获取其IP地址(我的是172.17.0.2,不同环境下可能不同)

然后在kali中启动python,导入pwntools库并且打开一个与Docker环境10001端口(即hello程序)的连接。

此时我们可以像上一篇文章一样打开IDA进行附加调试,在这里我就不再次演示了。从上一节的分析我们知道payload的组成应该是22个任意字符+地址。但是我们要怎么把16进制数表示的地址转换成4个字节的字符串呢?

我们可以选用structs库,当然pwntools提供了一个更方便的函数p32( )(即pack32位地址,同样的还有unpack32位地址的u32( )以及不同位数的p16( ),p64( )等等),所以我们的payload就是22*'A'+p32(0x0804846B)。

由于读取输入的函数是read,我们在输入时不需要以回车作为结束符(printf,getc,gets等则需要),我们使用代码io.send(payload)向程序发送payload。

由于我在这里没有设置IDA附加调试,显然程序也不会被断点中断,那么这个时候hello回显我们的输入之后应该成功地被payload劫持,跳转到getShell函数上了。为了与被pwn掉的hello进行交互,我们使用io.interactive( )

可以看到我们已经成功地pwn掉了这个程序,取得了其所在环境的控制权。为了增加一点气氛,我们在/home下面放了一个flag文件。让我们来看一下flag:

如图,我们成功的做出了第一个pwn题。为了加深对栈溢出的理解,我选了几个真实的CTF赛题作为作业,注意不要将思维固定在获取shell上哦。

课后例题和练习题非常重要,小伙伴请务必下载练习。后台回复“课后练习题”即可获得练习文档!

以上是今天的内容,大家看懂了吗?后面我们将持续更新Linux Pwn入门教程的相关章节,希望大家及时关注。

CTF必备技能丨Linux Pwn入门教程——栈溢出基础的更多相关文章

  1. CTF必备技能丨Linux Pwn入门教程——stack canary与绕过的思路

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  2. CTF必备技能丨Linux Pwn入门教程——PIE与bypass思路

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  3. CTF必备技能丨Linux Pwn入门教程——格式化字符串漏洞

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  4. CTF必备技能丨Linux Pwn入门教程——利用漏洞获取libc

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  5. CTF必备技能丨Linux Pwn入门教程——ROP技术(下)

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  6. CTF必备技能丨Linux Pwn入门教程——ROP技术(上)

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  7. CTF必备技能丨Linux Pwn入门教程——ShellCode

    这是一套Linux Pwn入门教程系列,作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的一些题目和文章整理出一份相对完整的Linux Pwn教程. 课程回顾>> Linu ...

  8. CTF必备技能丨Linux Pwn入门教程——环境配置

    说在前面 这是一套Linux Pwn入门教程系列,作者依据Atum师傅在i春秋上的Pwn入门课程中的技术分类,并结合近几年赛事中出现的一些题目和文章整理出一份相对完整的Linux Pwn教程. 问:为 ...

  9. CTF必备技能丨Linux Pwn入门教程——调整栈帧的技巧

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

随机推荐

  1. PHP配置篇(一)--php开启redis扩展

    因为最近要用到Redis,下面记录下如何给PHP开启redis的扩展. 一.安装redis 1.安装redis:https://github.com/MSOpenTech/redis/releases ...

  2. 环形链表 II

    给定一个链表,返回链表开始入环的第一个节点. 如果链表无环,则返回 null. 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始). 如果 pos 是 - ...

  3. linux-zookeeper-kafka入门

    公告:版权所有,违者必究 1.zookeeper安装 前提:先安装jdk,zookeeper运行依赖于java环境. (1.)下载安装包 http://mirror.bit.edu.cn/apache ...

  4. Mybatis工作原理(九)

    mybatis工作流程: (1) SqlSessionFactoryBuilder 从 XML 配置文件或通过Java的方式构建出 SqlSessionFactory 的实例. (2) SqlSess ...

  5. flask-windows部署

    由于supervisor不支持windows,但要防止程序异常中断,所以需要采取措施 通过pywin32,使得flask以服务的方式运行 创建pythonservice.py import win32 ...

  6. SpringCloud微服务常见组件理解

    概述 毫无疑问,Spring Cloud是目前微服务架构领域的翘楚,无数的书籍博客都在讲解这个技术.不过大多数讲解还停留在对Spring Cloud功能使用的层面,其底层的很多原理,很多人可能并不知晓 ...

  7. ASP.NET开发实战——(十三)ASP.NET MVC 与数据库之EF实体类与数据库结构

    大家都知道在关系型数据库中每张表的每个字段都会有自己的属性,如:数据类型.长度.是否为空.主外键.索引以及表与表之间的关系.但对于C#编写的类来说,它的属性只有一个数据类型和类与类之间的关系,但是在M ...

  8. MySQL实战45讲学习笔记:第四十讲

    一.本节概述 在上一篇文章中,我提到 MySQL 对自增主键锁做了优化,尽量在申请到自增 id 以后,就释放自增锁. 因此,insert 语句是一个很轻量的操作.不过,这个结论对于“普通的 inser ...

  9. 【django json.dumps 报错】 datetime.datetime is not JSON serializable

    django 中,json.dumps 无法直接转译 datetime 类型的值. 找了无数方法,找到一个最优.最简洁的解决办法: json.dumps(results, indent=4, sort ...

  10. (三十一)golang--面向对象之工厂模式

    golang面向对象中是不存在构造函数的,可以使用工厂模式.   使用工厂模式,让即使不是大写的变量可以被外部包使用.