这是一套Linux Pwn入门教程系列,做者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的一些题目和文章整理出一份相对完整的Linux Pwn教程。html
课程回顾>>Linux Pwn入门教程第一章:环境配置python
更多Pwn视频课程:https://www.ichunqiu.com/courses/pwn?from=weixindocker
本系列教程仅针对i386/amd64下的Linux Pwn常见的Pwn手法,如栈,堆,整数溢出,格式化字符串,条件竞争等进行介绍,全部环境都会封装在Docker镜像当中,并提供调试用的教学程序,来自历年赛事的原题和带有注释的python脚本。shell
教程中的题目和脚本如有使用不妥之处,欢迎各位大佬批评指正。小程序
今天是Linux Pwn入门教程第二章:栈溢出基础,阅读用时约10分钟。安全
函数的进入与返回bash
要想理解栈溢出,首先必须理解在汇编层面上的函数进入与返回。首先咱们用一个简单执行一次回显输入的程序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入门教程的相关章节,但愿你们及时关注。