MIPS缓冲区溢出学习实践

前言

之前在学习家用路由器这本书以及看网上大佬写相关文章 的时候,总感觉有些关键细节一笔带过,有时候给我造成了很大的困扰,鉴于这个原因,我想到把自己的一些思考以及实际操作经验写出来给后来者,希望他们不要再走我走过的弯路。

 

引爆内存崩溃

首先看源代码:

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

void do_system(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);
}

将vuln_system.c 拷贝至对应目录下:

执行如下命令:

root@ricard-virtual-machine:~/my_file# /root/my_file/buildroot1/buildroot/output/host/bin/mips-linux-gcc vuln_system.c -static -o vuln_system

root@ricard-virtual-machine:~/my_file# python -c "print 'A'*600">passwd

root@ricard-virtual-machine:~/my_file# qemu-mips vuln_system

而后会出现错误:

程序引发了一段故障,使用如下命令重新执行:

root@ricard-virtual-machine:~/my_file# qemu-mips vuln_system `python -c "print 'A'*600"`

这里直接运行,发生崩溃就退出了;

加-g是等待调试的:

root@ricard-virtual-machine:~/my_file# qemu-mips -g 1234 ./vuln_system `python -c "print 'A'*600"`

执行完这条指令之后,使用IDA进行附加调试:

这里选择大端是因为这个文件是mips大端格式的;

附加之后,在IDA里面按F9键(书里面写的是F5,,这是错的!)可以看到程序在试图执行0x41414141的时候崩溃了,如下图所示:

这是因为0x41414141将原来的返回地址给覆盖了,程序在返回的时候返回的是0x41414141这个无效地址而不是原来的地址,故会崩溃.

 

劫持流程

计算偏移

通过阅读vuln_system.c的源码可以知道,main函数里面,在读取完passwd这个文件之后,将passwd文件里面的所有数据存入堆栈的局部变量buf里面,而buf的大小仅为256字节,而passwd文件有600字节大小的数据写入buf,导致了缓冲区溢出;

通过静态分析发现,如果要使缓冲区溢出,并控制到堆栈中的返回地址saved_ra,需要覆盖的数据大小应该达到0x1A0-0x04即0x19c字节;作者这里运用这个公式的依据是什么呢?让我们回顾一下X86架构下的情形:

偏移不就是找buf和ra之间的偏移么,ra是存储于栈里面的(有点类似于x86里面的ret指令),buf指向栈里面,只要计算出buf的初始位置和ra之间的偏移,就可以计算出有多少个字节就可以溢出到ra了!

寻找偏移

上图是主函数里面的一开始的部分,为了进一步分析出偏移,笔者将相关汇编指令誊写并注释如下:

                 addiu   $sp, -0x1D0            //sp <==sp-0x1D0
                 sw      $ra, 0x1D0+var_4($sp)    //将ra里面的值存放于堆栈里面,其偏移值为0x1D0+var_4
                sw      $fp, 0x1D0+var_8($sp)        //将fp里面的值存放于堆栈里面,其偏移为0x1D0+var_8
                move    $fp, $sp                    //fp<== sp

                li      $gp, 0x4291A0                //li指令:将一个立即数存放于寄存器里面
                sw      $gp, 0x1D0+var_1C0($sp)    //将gp里面的值存放于堆栈里面,其偏移为0x1D0+var_1C0
                addiu   $v0, $fp, 0x1D0+var_1A0    //v0用于存放函数函数返回值
                li      $v0, 0x100                //将立即数0x100传入v0
                move    $a2, $v1                    //MIPS架构中一般使用a0-a3作为函数的前4个参数
                move    $a1, $zero                //zero寄存器里面永远为0
                move    $a0, $v0                    //a0=v0
                la      $v0, memset                //复制memset地址至至v0中
                move    $t9, $v0                    //$t0-$t9供汇编程序使用的临时变量
                bal     memset                    //无条件转移,并且将转移指令后面的第二条地址作为返回值存放于Ra里面
                nop

结合源代码看,可以发现主函数里面调用的第一个函数为memset函数,貌似源代码里面没有这个函数,怎么回事,继续看,发现在调用这个函数之前是调用了3个参数的,分别用到a0,a1,和a2这几个寄存器,(在MIPS架构里面,是用a0-a3这几个寄存器来传参的),而函数memset的原型为void* memset(void* s,int ch,unsigned n),其主要功能为:在内存空间里以s为起始的地方,将开始的n个字节设为指定值;可以发现传给a2的值为v1,而传给$v1的为0x100(0x100实际上就是十进制256),分析到这里,大家应该会清晰了吧,这里在做的事情其实是内存初始化,通过这个函数将内存里面的256个字节初始化为0,而这里的内存初始地址是通过指令addiu $v0,$fp,0x1D0+var_1A0来确定的,显然,0x1D0+var_1A0就是我们要找的buf的起始偏移,到这里,我们才能确定:需要覆盖的数据大小应该为0x1D0+var_1A0-0x1D0-var_4即0x19c字节;

验证

root@ricard-virtual-machine:~/my_file# python -c "print 'A'*0x19c + 'BBBB'+'CCCC'">passwd
​
root@ricard-virtual-machine:~/my_file# qemu-mips -g 1234 vuln_system
​

输入指令之后,程序就会处于等待调试的状态; 而后利用IDA附加该进程(此过程在前面已经叙述过);

由于这里使用附加调试的效果不是太好,我这里使用的运行时调试的方法,读者可以参考这本书即可;

在主函数结尾的地方下断点,按F9运行程序,会在0x004006CC这个地址断下:

双击0x004006D0这行;来到返回地址在栈空间0x40800104(也可以利用SP+0x1D0+VAR_4得到)处,如下图 所示:

查看HEX VIEW-1窗口,发现返回地址已经被覆盖为0x42424242,如下图所示,此时缓冲区已经被输入的数据所覆盖,并且越界后覆盖了堆栈上的其他数据;

继续按F8键执行指令jr $ra,程序就会跳往0x42424242出执行,如下图所示:

 

小结

在这一节里面,主要学习的知识点是如何计算偏移达到覆盖返回地址的目的,这里总结出一个公式:

偏移=函数返回地址-缓冲区首地址;

(注:在堆栈中,一般函数返回地址处于高地址,缓冲区地址处于低地址),今天就暂时写到这里,后面有时间我会带来更多的分享.

(完)