继上文点到栈溢出的基础原理,本文来详细演示如何完成一题栈溢出
checksec的使用及保护机制了解
操作系统提供了许多安全机制来尝试降低或阻止缓冲区溢出攻击带来的安全风险
在我们开始尝试做题之前,我们先得知道这道题开启了哪些安全机制,我们又该如何绕过其安全机制
这里便使用到了checksec工具
Arch:
程序架构信息。判断是拖进64位IDA还是32位?exp编写时p64还是p32函数?
RELRD:
Relocation Read-Only (RELRO) 此项技术主要针对 GOT 改写的攻击方式。它分为两种,Partial RELRO 和 Full RELRO。
部分RELRO 易受到攻击,例如攻击者可以atoi.got为system.plt,进而输入/bin/sh\x00获得shell
完全RELRO 使整个 GOT 只读,从而无法被覆盖,但这样会大大增加程序的启动时间,因为程序在启动之前需要解析所有的符号。
(看不懂没有关系 ,后续学习将会接触到plt和got表的相关知识)
Stack:
由上文可知,在我们进行栈溢出的时候,只需覆盖ret addr就能达到操控程序执行流的目的,但此项保护机制,在栈中会随机生成一段数据,在函数返回的时候,会检验这段数据是否正确,如果不正确,程序就会崩溃退出,这段数据在liunx中被称为canary
NX:
NX enabled如果这个保护开启就是意味着我们对栈中数据没有执行权限,我们无法在栈中自由更写,但是通过ROP构造执行流的办法可以绕过这个保护(同样将在接下来的学习中讲到)
PIE:
pie保护机制和ASLR相类似,我们已经知道每个数据在计算机中都有自己相应的地址,通过寻址计算机可以成功调用他们,ASLR 将程序运行时的堆栈以及共享库的加载地址随机化,而 PIE 技术则在编译时将程序编译为位置无关, 即程序中的地址在每次运行时都会发生变化,我们将在后续的学习中接触到pie
ida使用和漏洞分析
通过checksec我们可以得知此题为32位,我们将其拖进32位ida查看
左边的function name是此二进制文件中的函数列表,通常主函数的名称是main
右侧便为此文件的汇编代码
我们可以按下f5进行ida反编译,将其转化为c语言代码的形式
main函数的组成相当简单,只有一个子函数shell,我们点进去看看
第三行,定义了一个buf变量,那么后面的esp+0h和ebp-48h是什么意思?
我们进行栈溢出的目的是为了覆盖ret addr 从而将程序的返回地址修改成我们需要的,此时read读入的变量buf的位置就是位于ebp再往上0x48处的栈顶
所以我们可以得知,我们赋值的变量buf离ret addr差了0x48+0x4个字节
这里为什么还要加4?如果是64位呢,还是加4吗
我们之前说过了,一个字长对应的字节是根据位数的不同来决定的
此时需要在0x48的数据上再加上0x4是因为我们离ret addr还隔着一个ebp,一个字长的距离
接下来我们看回程序,输出语句”welcome to NISACTF”
在return处调用了函数read,读入了0x60字长的数据,而此时我们需要溢出的数据长度为0x48+0x4+0x4(我们用来覆盖ret addr的数据)
这里就出现了栈溢出漏洞,但是此时我们还需要system(/bin/sh)的地址,以达到获取flag的目的
我们可以依法炮制,翻阅其他函数的内容,我们发现这题的作者并没有直接给我们
那么system(/bin/sh)就需要我们自己构造
我们接着使用shift+f12来打开string窗口,一键找出所有的字符串
我们惊喜的发现,在data段(不清楚什么是data段的,请仔细观看谢师傅的视频,为虚拟内存映射的相关知识)中存在字符串/bin/sh
拥有这段字符串意味着什么?
我们知道,函数的执行一般需要参数
例如system(/bin/sh)中的/bin/sh便是system的参数
所以,我们接下来的任务就是去找到system函数的地址,并且把/bin/sh这段字符串作为参数,就可以实现system(/bin/sh)
plt表和got表
这里举一个简单的例子
某公司开发了一款软件,实现的代码调用了大量的函数,这些函数的每次实现都需要在文件(这个软件)中对应的前置来实现他
这样子就增大了文件的体积
但是如果将实现这些函数的前置在程序运行时载入到内存中呢?
这样每一款软件都无需额外的内存占用,并且用户也能成功运行
所以这里就出现了plt表和got表的概念
当我们需要调用system函数时,他会去自动寻找system的plt表
其plt表中存储了system函数的got表的地址
其就是system在libc(libc同样在谢师傅的视频中有所介绍)中的地址
但是我们在第一次调用system函数的时候,其got表中的地址并不知道system在libc中的哪里
于是第一次调用无果后,system的plt表便会收到消息,去给got表找到system在libc的位置
经过原定好的代码实现,plt表便查找到了libc中system的地址,并将其填充给了got表
接下来plt表再去got表中,他就得知了地址,就可以成功调用system函数
所以我们可以将整个流程用下图来展示
而在之前程序已经调用了system函数进行echo 字符串,所以我们此时再去system的plt表中就能获取到system在libc中的地址
这里你可能会无法理解,但是在初期的学习我们只需要记住,plt表是调用该函数,got表中存的地址是为了获得真实地址
发现漏洞后,接下来就要进行shellcode的编写,开始攻击
以下shellcode的编写需要python基础,这里同样不做解释,请自行学习)
但我个人是没有进行任何额外的python学习(截止到我学习到堆),不知道这条路是否合理化,请根据自己在学习中遇到的情况自行斟酌
exp的编写
–第二行建立与靶场的联系(概念不做解释,自行查找)的联系,名称不一定要是io,“ ”中为ip地址,逗号后为端口)
–第三行为后续我们获得system的plt表内容的前置条件,括号中为”./文件名”
–第四行,接收程序的输出内容,简单的是xxx.recv() [xxx为你先前命名的名称,比如我的io]
io.recv()将会接收一行的数据,以\n为结束判定
而io.recvuntil(“ “)将会接收到” “内的数据才停止,并且包括“ ”内的数据
如果不想接收“ ”内的数据,可以这样编写io.recvuntil(“hello”,drop = True)
–第五行,为变量system_addr赋值
赋值的内容为system的plt表中的内容
–第六行,编写payload,cyclic创建括号内自定义字长的垃圾数据用来填充ret addr前的内容
随后我们要注意p32()这个语法,为什么要使用它?
首先,send系列只能传参字符串,recv接收回来的也只能是字符串,这是因为网络传输的规定!
所以,对于地址值0x1234,就只有变成字符串传出去
你应该也能猜到为什么是p32,64位程序使用的便是p64
接下来我们可以理解p32(0x804a024)就是字符串/bin/sh的地址,那么中间的0xabcdabc又是什么?
这里先粗略进行一个介绍,具体的介绍将在后续进行一个专门的文章(可见其中的复杂)
我们只需要知道,正常的调用system(即非使用call system的汇编代码地址),要想成功为其传参,函数和参数之间要有一个垃圾数据
所以他的形式可以不为abcdabc可以是任何符合格式的地址
–第七行,发送我们构造好的payload,进行栈溢出攻击
最后,io.interactive()来与终端交互,如果pwn成功,那么我们就可以使用ls来查看当前目录下有哪些文件
我们利用python3(需要你的虚拟机中安装好了python3)发送这段exp试试
可以看到我们输入ls后,成功列出了目录的文件,我们发现了一个名为flag的文件,那么这就是我们的最后答案了
cat flag(liunx使用命令请自行百度) 答案到手!
在阅读完本文后,相信你对栈溢出的解法已经有了一个大致的理解,接下来请自行前往ctfshow或者其他平台独立解题