20191218 Exp1-逆向破解实验
逆向及Bof基础实践说明
实践目标
本次实践的对象是一个名为pwn1的linux可执行文件。
该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
-
该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。正常情况下这个代码是不会被运行的。
-
此次实践的目标就是想办法运行我们提前准备好的这个代码片段,以达到获取中断权限的目的。本次实践中将使用两种方法运行这个代码片段,然后学习如何注入运行任意Shellcode。
实践内容
- 手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
- 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
- 注入一个自己制作的shellcode并运行这段shellcode。
实践思路
- 运行原本不可访问的代码片段
- 强行修改程序执行流
- 实现注入shellcode攻击,并能够运行任意shellcode
BOF原理
基础知识准备
- x86 32位常见寄存器
寄存器 | 名称 | 功能 |
---|---|---|
EAX | 累加(Accumulator)寄存器 | 常用于乘、除法和函数返回值 |
EBX | 基址(Base)寄存器 | 常做内存数据的指针, 或者说常以它为基址来访问内存 |
ECX | 计数器(Counter)寄存器 | 常做字符串和循环操作中的计数器 |
EDX | 数据(Data)寄存器 | 常用于乘、除法和 I/O 指针 |
ESI | 来源索引(Source Index)寄存器 | 常做内存数据指针和源字符串指针 |
EDI | 目的索引(Destination Index)寄存器 | 常做内存数据指针和目的字符串指针 |
ESP | 堆栈指针(Stack Point)寄存器 | 只做堆栈的栈顶指针; 不能用于算术运算与数据传送 |
EBP | 基址指针(Base Point)寄存器 | 只做堆栈指针, 可以访问堆栈内任意地址, 经常用于中转 ESP 中的数据, 也常以它为基址来访问堆栈; 不能用于算术运算与数据传送 |
EIP | 指令指针(Instruction Pointer)寄存器 | 总是指向下一条指令的地址; 所有已执行的指令都被它指向过 |
-
使用到的机器码
-
NOP
空指令,机器码0x90
。NOP指令什么也不做,向后面的指令继续执行。 -
JNE
条件转移指令,机器码0x75
。> jump if not equal,不相等则跳转。 -
JE
条件转移指令,机器码0x74
。> jump if equal,相等则跳转。 -
JMP
无条件转移指令,短跳转机器码0xEBEB
,近跳转机器码0xE9
,间接转移机器码0xFF
远跳转机器码0xEA
-
CMP
比较指令,机器码0x39
。目标操作数-源操作数,不保存结果
-
-
反汇编基础
通过反汇编查找含有跳转指令的汇编行,修改该部分的机器代码使之跳转至getShell函数(其中getShell等函数地址也通过反汇编查询)。-
objdump
objdump -d [elf file]
objdump
是gcc的工具,用于解析二进制目标文件。其中-d
模式可反汇编文件。 -
gdb
在gdb模式下使用disass [func]
可对func
指定函数进行反汇编。
-
-
十六进制编辑器
- vim
十六进制编辑器有很多种,本次实践采用的是广泛使用的vim
编辑器。其中,在命令模式下,输入:%!xxd
可以将文本以十六进制形式显示。输入:%!xxd -r
可以将内容转化为十六进制的信息转换回二进制显示。
- vim
-
输入输出重定向
- 输入重定向
输入方向就是数据从哪里流向程序。数据默认从键盘流向程序,如果改变了它的方向,数据就从其它地方流入。 - 输出重定向
输出方向就是数据从程序流向哪里。数据默认从程序流向显示器,如果改变了它的方向,数据就流向其它地方。
- 输入重定向
-
管道
- 在Linux中,用"|"符号来连接两个命令,以前面命令的标准输出作为后面命令的标准输入,例如
ls -l | more
,该命令列出当前目录中的任何文档,并把输出送给more
命令作为输入,more
命令分页显示文件列表 - 管道命令必须是接受标准输出的命令,
cp
、mv
、ls
等都不是管道命令
- 在Linux中,用"|"符号来连接两个命令,以前面命令的标准输出作为后面命令的标准输入,例如
实验过程
直接修改程序机器指令,改变程序执行流程
-
将
pwn1
移动到20191218tangqiheng
文件夹
-
查看反汇编代码
使用objdump -d pwn1 | more
-
找到
getShell
、foo
、main
函数的指令,发现指令call 8048491 <foo>
,说明main
函数在0x80484b5
位置调用了foo
函数。
而我们需要调用getShell
函数来弹出可用shell。 -
为了调用
getShell
函数,我们可以直接修改main
函数部分的调用foo
函数机器指令,使它调用getShell
函数。
-
-
计算偏移量
call
指令的机器码是e8
,而0xffffffd7
为call指令的下一条指令地址0x80484ba
与foo
函数起始地址0x8048491
间偏移量的补码。
因此,我们只需要修改0xffffffd7
为call
指令的下一条指令地址0x80484ba
与getShell
函数起始地址0x804847d
间偏移量的补码即可。
用程序员模式的计算器计算出偏移量的补码为0xffc3
(只取低32位即可,高位均为符号填充) -
修改机器指令
根据上一步计算结果,用vim编辑器将0x80484b5
处的指令改为0xe8c3ffffff
即可(注意小端机器存储方式),vim
打开pwn1
,命令模式下输入:%!xxd
显示16进制文件输入/e8 d7
指令找到call foo
指令位置
修改偏移量
发现原来call foo
已变成了call getshell
输入:%!xxd -r
转回二进制文件
输入:wq
保存并退出
-
执行pwn1文件验证
执行./pwn1
后发现getshell
函数起作用,成功获取shell
通过构造输入参数,造成BOF攻击,改变程序执行流
将修改之前的pwn1
重命名为pwn2
,并转移到同样的文件夹下
-
通过反汇编后的汇编代码取程序基本功能的信息
观察汇编代码,得知在foo
函数执行时,在进行gets
输入前会预留0x1c
(即十进制的数字28)字节大小的缓冲区来存取输入值,但gets
函数中并没有进行输入长度检查。
而main
函数在call foo
的同时会将返回地址(原先的EIP)0x80484ba
压入栈中。所以如果将getShell
函数的入口地址0x804847d
覆盖此处,在foo函数执行结束后就会跳转到getShell
函数中而不是0x80848ba
,从而达到攻击的目的。 -
确认覆盖到返回地址的字符串字符
- 用
gdb
调试pwn2
发现系统还未安装gdb
,使用sudo apt-get install gdb
进行安装
安装成功,继续刚才的调试
输入r
运行,输入1111111122222222333333334444444412345678
,其中前32字节为11111111222222223333333344444444
,刚好覆盖栈中缓冲区的28字节与其后的EBP(4字节),接下来的1234
应覆盖返回地址(原先的EIP)0x80484ba
。覆盖后由于找不到地址1234,foo
执行结束后会报错,此时EIP应为错误的返回地址,输入info r
,查看寄存器EIP的值
此时EIP为0x34333231
,即为1234
的ASCII码的表示(小端)。
由此,结合之前反汇编时得到的getShell
的内存地址0x0804847d
和1234
对应0x34333231
,我们应当输入11111111222222223333333344444444\x7d\x84\x04\x08
- 用
-
构造输入字符串
由于我们没法通过键盘输入\x7d\x84\x04\x08
这样的16进制值,所以需要生成包括这样字符串的一个文件。其中\x0a
表示回车,如果没有的话,在程序运行时就需要手工按一下回车键。
根据Perl语言特性(\x7d\x84\x04\x08
将直接转换成对应16进制数),我们可以用perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input
实现目的。使用16进制查看指令xxd查看input文件
(cat input; cat) | ./pwn1
将input的输入,通过管道符“|”,作为pwn1
的输入
攻击成功
注入Shellcode并执行
-
shellcode基础
- shellcode就是一段机器指令
- 通常这段机器指令的目的是为获取一个交互式的shell(像linux的shell或类似windows下的cmd.exe),所以这段机器指令被称为shellcode。
- 在实际的应用中,凡是用来注入的机器指令段都通称为shellcode,像添加一个用户、运行一条指令。
更多有关shellcode的基础知识学习可以参考老师提供的网站
手把手简易实现shellcode及详解
本次实验直接使用已经构造好的shellcode,代码如下
\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
- shellcode就是一段机器指令
-
准备工作
参考项目负责人同学给出的方法安装execstack
命令
-
execstack -s pwn2
设置堆栈可执行
-
关闭地址随机化
查看当前情况
发现randomize_va_space
为2,即地址随机化保护是开启的
尝试修改却发现权限不够,于是用root
打开shell
-
-
构造要注入的payload
Linux下有两种基本构造攻击buf的方法:- retaddr+nop+shellcode
- nop+shellcode+retaddr。
因为retaddr在缓冲区的位置是固定的,shellcode要不在它前面,要不在它后面。简单说缓冲区小就把shellcode放后边,缓冲区大就把shellcode放前边。此处我们采用的是第二种方法,即结构为:nops+shellcode+retaddr。
-
nop一为是了填充,二是作为“着陆区/滑行区”。
-
我们猜的返回地址只要落在任何一个nop上,自然会滑到我们的shellcode。
-
输入如下命令
接下来确定\x4\x3\x2\x1
处具体该填什么。 -
注入攻击buf
(cat input;cat) | ./pwn2
-
同时开另外一个终端,用gdb来调试pwn1这个进程
首先找到pwn2
对应进程号,为1653
启动gdb
,attach上该进程
根据foo
函数反汇编返回地址设置断点
继续执行,到断点处停下,查看ESP寄存器信息
根据ESP的值0xffffd14c
进一步查看注入字符串所处地址
找到0x90909090
所在地址0xffffd12c
,确定原有\x4\x3\x2\x1
的值。然而注入发现攻击失败,仔细思考分析后发现其实原有shellcode存在问题,原来分析得到的地址错误
-
修改shellcode,先用32个‘A’覆盖掉缓冲区加ESB,再结合之前的步骤找到字符串需要覆盖的真正的地址,即
01020304
紧挨着的地址0xffffd150
我们需要修改的是把原有shellcode开头的\x90\x90\x90\x90
改成\x50\xd1\xff\xff
,再次尝试攻击
发现成功获取shell,攻击成功
结合nc模拟远程攻击
-
实验环境
-
靶机
- Kali 2021
- NAT模式
- IP
192.168.174.141
-
攻击机
- OpenEuler 21.09
- NAT模式
- IP
192.168.174.138
-
-
攻击过程
-
靶机上运行
nc -lvnp 1218 -e ./pwn2
,运行pwn2
程序并打开1218
端口
-
攻击机上将之前的shellcode输出到input,再使用
(cat input; cat) | nc 192.168.174.141 1218
-
攻击成功
-
实验总结
- 本次实验跟本学期的课设题目之一很相似,因此我得到了参与缓冲区溢出攻击课程设计的同学的不少帮助,同时借助自己上网查阅相关的博客,我也慢慢学会了缓冲区溢出的原理,并尝试自己在Ubuntu、Debian、OpenEuler等系统上实现简单的缓冲区溢出攻击。
缓冲区溢出是指程序试图向缓冲区写入超出预分配固定长度数据的情况。这一漏洞可以被恶意用户利用来改变程序的流控制,甚至执行代码的任意片段。这一漏洞的出现是由于数据缓冲器和返回地址的暂时关闭,溢出会引起返回地址被重写。
- 在本次实验中,我对缓冲区溢出的基本原理及预防方法有所掌握,同时,在实践中理解了实验指导书中的三个攻击实例,也了解到了缓冲区溢出攻击的危害。缓冲区溢出是指当计算机向缓冲区内填充数据时超过了缓冲区本身的容量溢出;某些情况下,溢出的数据只是覆盖在一些不太重要的内存空间上,不会产生严重后果;但是一旦溢出的数据覆盖在合法数据上,可能给系统带来巨大的危害。
所以,我们要编写风格良好的代码,积极检查边界,不让攻击者执行缓冲区内的命令,利用程序指针检查。