MIPS汇编学习

MIPS汇编学习

  mips汇编不同于x86汇编,属于精简指令集,常见于路由器等一些嵌入式设备中。

  mips汇编没有对堆栈的直接操作,也就是没有push和pop指令,mips汇编中保留了32个通用寄存器,但是不同于x86汇编,mips汇编中没有ebp/rbp寄存器。

  mips每条指令都用固定的长度,每条指令都是四个字节,所以内存数据的访问必须以32位严格对齐,这一点也不同于x86汇编。

  通过一个demo,用mips-linux-gnu-gcc编译,通过IDA远程调试,来理解mips汇编中的一些概念。

#include<stdio.h>

int sum(int a,int b){
    return a+b;
}

int main()
{
    int a=1,b=2,c;
    c=sum(a,b);
    printf("%d\n",c);

    return 0;
}

32个通用寄存器的功能和使用约定定义如下:

mips汇编中重要的寄存器:

   1.堆栈指针$sp,也就是$29指向堆栈的栈顶,类似于x86中的ebp和rbp指针;

  2.$0寄存器的值始终为常数0;

  3.PC寄存器保留程序执行的下一条指令,相当于x86架构中的eip寄存器;

  4.参数传递的时候,$a0-$a3寄存器保存函数的前四个参数,其他的参数保存在栈中;

  5.$ra寄存器,保存着函数的返回地址,这一点也不同于x86汇编中将返回地址保存在栈中。在函数A执行到调用函数B的指令时,函数调用指令复制当前的$PC寄存器的值到$RA寄存器,然后跳转到B函数去执行,即当前$RA寄存器的值就是函数执行结束时的返回地址。

  如上图所示,调用sum函数之前,$ra寄存器的值是0x7f62eca8。

  进入分支延迟槽之后,$ra寄存器的值被赋值为$pc寄存器的下一条指令地址。在结束sun函数调用之后,通过:jr  $ra指令跳转回main函数继续执行。

  5.mips架构下,对静态数据段的访问,通过$gp寄存器配合基址寻址来实现;

  7.$30寄存器表示帧指针,指向正在被调用的栈桢,mips和x86由于堆栈结构的区别,调用栈时会出现一些不同。mips硬件并不直接支持堆栈,x86有单独的push和pop指令,但是mips没有栈操作指令,所有对栈的操作都是统一的内存访问方式。

x86中,栈桢入口点开辟栈桢的操作:

push ebp
mov ebp,esp
sub esp,0x30

x86中,栈桢出口点退栈的操作:

leave    #  push ebp
         #  mov ebp,esp
ret      #  pop eip

  由以上函数入口点和出口点的操作我们可以清晰地看出,x86架构中,同一个函数中,esp始终指向函数调用栈的栈顶,ebp始终指向函数调用栈的栈基。

  但是在mips架构下,没有指向栈基的寄存器,这时候如何确定函数调用的栈桢呢?

  $fp(帧指针)和  $sp(栈指针)  来确定函数的调用栈。$sp寄存器作为堆栈寄存器,始终指向栈顶。进入一个函数是,需要将当前栈指针向下移动n字节,这个大小为n字节的存储空间就是此函数的Stack Frame的存储区域,此后栈指针便不再移动,只通过函数返回是将栈指针加上偏移量恢复栈现场。由于不能随便移动栈指针,所以寄存器压栈和出栈时都要指定偏移量。

         可以看到,mips架构中,在函数入口处以addiu   $sp,-0x30来开辟栈桢,当程序运行到0x400770地址处时,$fp寄存器的值被保留到了栈上,$fp寄存器的值为0。

   继续单步执行,看到将$sp寄存器的值赋值给了$fp寄存器,这时候堆栈寄存器和帧指针同时指向当前调用栈的栈顶。

   继续单步执行,0x40079c地址处,我们在sum函数的入口处下一个断点,由于mips架构的分支延迟机制,nop指令就是一个分支延迟槽。执行完nop指令之后,接下来我们会步进到sum函数中,进入sum函数之后,我们再来观察$sp寄存器和$fp寄存器的变化情况。

   可以看到,addiu指令再次开辟了8字节大小的栈桢。

   单步执行到0x40073c地址处,$fp寄存器的值在赋值前被保留在栈上,$fp寄存器被再次赋值,指向当前调用栈的栈顶。

   结束函数调用之后,$fp和$sp还原为指向main函数调用栈的栈顶。可以看到,$fp寄存器主要用来进行基址寻址。所有针对栈区变量的取址,都通过基址寻址来对内存进行访问。

mips汇编函数调用过程中与x86架构的区别:

  1. mips架构和x86架构中,栈的增长方向相同,都是从高地址向低地址增长,但是没有栈底指针,所以调用一个函数是,需要将当前栈向低地址处移动n比特这个大小为n比特的空间就是此函数的栈桢存储区域;
  2. mips架构中有叶子函数和非叶子函数的区别,叶子函数就是此函数自身不再调用别的函数,非叶子函数就是此函数自身调用别的函数。如果函数A调用函数B,调用者函数会在自己的栈顶预留一部分空间来保存被调用者(函数B)的参数,称之为参数调用空间;
  3. 函数调用过程中,父函数调用子函数,复制当前$PC的值到$RA寄存器,然后跳转到子函数执行。到子函数是,子函数如果为非叶子函数,则子函数的返回地址会先存入堆栈
  4. 参数传递方式,前四个参数通过$a0-$a3来传递,多于的参数会放入调用参数空间(参数会被保存在栈上),可以类比x86_64参数传递规则来进行记忆。

mips架构的四种取址方式:

  1.基址寻址;(load-store)

    根据我们上面的例子可以看到,基址寻址是对mips架构下堆栈数据进行存储和加载的主要方式。

  2.立即数寻址;(load-store)

  3.寄存器寻址;(R型指令)

  4.伪立即数寻址;(J型指令)

叶子函数和非叶子函数:

  一个函数如果不再调用其他的函数,那么这个函数是叶子函数,一个函数如果调用其他的函数,那么这个函数是非叶子函数。一般来说,函数都是非叶子函数。

  叶子函数和非叶子函数在存放返回地址的时候,存在差异。叶子函数只把返回地址保存在$ra寄存其中,结束函数调用的时候,通过jr $ra指令返回即可。非叶子函数把在函数调用初始把$ra寄存器中的返回地址保存在栈中,然后结束函数调用的时候将栈中保存的返回地址加载到$ra寄存器中,再通过jr $ra指令返回。

  举例如下:

  叶子函数函数入口:

   非叶子函数函数入口:

   叶子函数函数出口:

   非叶子函数函数出口:

   叶子函数和非叶子函数的差别,造成栈溢出漏洞利用的差别。对于非叶子函数而言,如果我们的溢出可以覆盖栈上保存的$ra寄存器的值,这时候在栈上的值返回给$ra寄存器的时候,我们就可以劫持程序的数据流。

   通过《揭秘家用路由器0day漏洞挖掘技术》这本书中的一个例子来展示MIPS32架构下函数的参数传递及堆栈布局的变化:

#include<stdio.h>

int more_reg(int a,int b,int c,int d,int e)
{
               char dst[100]={0};
               sprintf(dst,"%d%d%d%d%d\n",a,b,c,d,e);  
}                

int main()
{
                int a1=1,a2=2,a3=3,a4=4,a5=5;
                more_reg(a1,a2,a3,a4,a5);
                return 0;
}

  静态链接,编译选项:

mips-linux-gnu-gcc -o demo1 demo1.c -static

   由上图看到,函数传递了5个参数,前四个参数首先保存在栈上,然后在调用more_reg函数的时候,把栈区的变量传递到$a0-$a3这四个寄存器中。其中第五个参数保留在0x40+var_30($sp)这个地址处。可以看到,虽然函数more_reg的前四个参数是由$a0-$a3这四个寄存器传递的,但是栈区仍然保留了16个字节的参数空间,就是0x40+var_20($fp)到0x40+var_30($fp)这段空间。

  断点0x400660处,就是将变量值从临时变量$v0中取出,存储到0x40+var_30($fp)处,然后0x40+var_30($fp)作为第五个参数传递到more_reg函数中去。

MIPS系统调用

  mips架构中,syscall用于从内核请求服务。对于MIPS,必须在$v0中传递服务编号/代号,然后将参数赋值给$a0,$a1,$a2三个,然后使用syscall指令触发中断,来调用相应函数。

  如果linux中搭建了mips的交叉编译环境的话,mips的系统调用号可以在/usr/mips-linux-gnu/include/asm/unistd.h中看到,调用号是从4000开始的。

#define __NR_Linux            4000
#define __NR_syscall            (__NR_Linux +    0)
#define __NR_exit            (__NR_Linux +    1)
#define __NR_fork            (__NR_Linux +    2)
#define __NR_read            (__NR_Linux +    3)
#define __NR_write            (__NR_Linux +    4)
#define __NR_open            (__NR_Linux +    5)
#define __NR_close            (__NR_Linux +    6)
#define __NR_waitpid            (__NR_Linux +    7)
#define __NR_creat            (__NR_Linux +    8)
#define __NR_link            (__NR_Linux +    9)
#define __NR_unlink            (__NR_Linux +  10)
#define __NR_execve            (__NR_Linux +  11)
#define __NR_chdir            (__NR_Linux +  12)
#define __NR_time            (__NR_Linux +  13)
#define __NR_mknod            (__NR_Linux +  14)
#define __NR_chmod            (__NR_Linux +  15)
#define __NR_lchown            (__NR_Linux +  16)
#define __NR_break            (__NR_Linux +  17)
#define __NR_unused18            (__NR_Linux +  18)
#define __NR_lseek            (__NR_Linux +  19)
#define __NR_getpid            (__NR_Linux +  20)
#define __NR_mount            (__NR_Linux +  21)
#define __NR_umount            (__NR_Linux +  22)
#define __NR_setuid            (__NR_Linux +  23)
#define __NR_getuid            (__NR_Linux +  24)
#define __NR_stime            (__NR_Linux +  25)
#define __NR_ptrace            (__NR_Linux +  26)
#define __NR_alarm            (__NR_Linux +  27)
#define __NR_unused28            (__NR_Linux +  28)
#define __NR_pause            (__NR_Linux +  29)
#define __NR_utime            (__NR_Linux +  30)
#define __NR_stty            (__NR_Linux +  31)
#define __NR_gtty            (__NR_Linux +  32)
#define __NR_access            (__NR_Linux +  33)
#define __NR_nice            (__NR_Linux +  34)
#define __NR_ftime            (__NR_Linux +  35)
#define __NR_sync            (__NR_Linux +  36)
#define __NR_kill            (__NR_Linux +  37)
#define __NR_rename            (__NR_Linux +  38)
#define __NR_mkdir            (__NR_Linux +  39)
#define __NR_rmdir            (__NR_Linux +  40)
#define __NR_dup            (__NR_Linux +  41)
#define __NR_pipe            (__NR_Linux +  42)
#define __NR_times            (__NR_Linux +  43)
#define __NR_prof            (__NR_Linux +  44)
#define __NR_brk            (__NR_Linux +  45)
#define __NR_setgid            (__NR_Linux +  46)
#define __NR_getgid            (__NR_Linux +  47)
#define __NR_signal            (__NR_Linux +  48)
#define __NR_geteuid            (__NR_Linux +  49)
#define __NR_getegid            (__NR_Linux +  50)
#define __NR_acct            (__NR_Linux +  51)
#define __NR_umount2            (__NR_Linux +  52)
#define __NR_lock            (__NR_Linux +  53)
#define __NR_ioctl            (__NR_Linux +  54)
#define __NR_fcntl            (__NR_Linux +  55)
#define __NR_mpx            (__NR_Linux +  56)
#define __NR_setpgid            (__NR_Linux +  57)
#define __NR_ulimit            (__NR_Linux +  58)
#define __NR_unused59            (__NR_Linux +  59)
#define __NR_umask            (__NR_Linux +  60)
#define __NR_chroot            (__NR_Linux +  61)
#define __NR_ustat            (__NR_Linux +  62)
#define __NR_dup2            (__NR_Linux +  63)
#define __NR_getppid            (__NR_Linux +  64)
#define __NR_getpgrp            (__NR_Linux +  65)
#define __NR_setsid            (__NR_Linux +  66)
#define __NR_sigaction            (__NR_Linux +  67)
#define __NR_sgetmask            (__NR_Linux +  68)
#define __NR_ssetmask            (__NR_Linux +  69)
#define __NR_setreuid            (__NR_Linux +  70)
#define __NR_setregid            (__NR_Linux +  71)
#define __NR_sigsuspend            (__NR_Linux +  72)
#define __NR_sigpending            (__NR_Linux +  73)
#define __NR_sethostname        (__NR_Linux +  74)
#define __NR_setrlimit            (__NR_Linux +  75)
#define __NR_getrlimit            (__NR_Linux +  76)
#define __NR_getrusage            (__NR_Linux +  77)
#define __NR_gettimeofday        (__NR_Linux +  78)
#define __NR_settimeofday        (__NR_Linux +  79)
#define __NR_getgroups            (__NR_Linux +  80)
#define __NR_setgroups            (__NR_Linux +  81)
#define __NR_reserved82            (__NR_Linux +  82)
#define __NR_symlink            (__NR_Linux +  83)
#define __NR_unused84            (__NR_Linux +  84)
#define __NR_readlink            (__NR_Linux +  85)
#define __NR_uselib            (__NR_Linux +  86)
#define __NR_swapon            (__NR_Linux +  87)
#define __NR_reboot            (__NR_Linux +  88)
#define __NR_readdir            (__NR_Linux +  89)
#define __NR_mmap            (__NR_Linux +  90)
#define __NR_munmap            (__NR_Linux +  91)
#define __NR_truncate            (__NR_Linux +  92)
#define __NR_ftruncate            (__NR_Linux +  93)
#define __NR_fchmod            (__NR_Linux +  94)
#define __NR_fchown            (__NR_Linux +  95)
#define __NR_getpriority        (__NR_Linux +  96)
#define __NR_setpriority        (__NR_Linux +  97)
#define __NR_profil            (__NR_Linux +  98)
#define __NR_statfs            (__NR_Linux +  99)
#define __NR_fstatfs            (__NR_Linux + 100)
#define __NR_ioperm            (__NR_Linux + 101)
#define __NR_socketcall            (__NR_Linux + 102)
#define __NR_syslog            (__NR_Linux + 103)
#define __NR_setitimer            (__NR_Linux + 104)
#define __NR_getitimer            (__NR_Linux + 105)
#define __NR_stat            (__NR_Linux + 106)
#define __NR_lstat            (__NR_Linux + 107)
#define __NR_fstat            (__NR_Linux + 108)
#define __NR_unused109            (__NR_Linux + 109)
#define __NR_iopl            (__NR_Linux + 110)
#define __NR_vhangup            (__NR_Linux + 111)
#define __NR_idle            (__NR_Linux + 112)
#define __NR_vm86            (__NR_Linux + 113)
#define __NR_wait4            (__NR_Linux + 114)
#define __NR_swapoff            (__NR_Linux + 115)
#define __NR_sysinfo            (__NR_Linux + 116)
#define __NR_ipc            (__NR_Linux + 117)
#define __NR_fsync            (__NR_Linux + 118)
#define __NR_sigreturn            (__NR_Linux + 119)
#define __NR_clone            (__NR_Linux + 120)
#define __NR_setdomainname        (__NR_Linux + 121)
#define __NR_uname            (__NR_Linux + 122)
#define __NR_modify_ldt            (__NR_Linux + 123)
#define __NR_adjtimex            (__NR_Linux + 124)
#define __NR_mprotect            (__NR_Linux + 125)
#define __NR_sigprocmask        (__NR_Linux + 126)
#define __NR_create_module        (__NR_Linux + 127)
#define __NR_init_module        (__NR_Linux + 128)
#define __NR_delete_module        (__NR_Linux + 129)
#define __NR_get_kernel_syms        (__NR_Linux + 130)
#define __NR_quotactl            (__NR_Linux + 131)
#define __NR_getpgid            (__NR_Linux + 132)
#define __NR_fchdir            (__NR_Linux + 133)
#define __NR_bdflush            (__NR_Linux + 134)
#define __NR_sysfs            (__NR_Linux + 135)
#define __NR_personality        (__NR_Linux + 136)
#define __NR_afs_syscall        (__NR_Linux + 137) /* Syscall for Andrew File System */

  可以写一个write.c的,然后生成汇编代码,看一下write函数的系统调用。

#include<stdio.h>
#include<stdlib.h>

int main()
{
    char *dst="Hello world!\n";
    write(1,dst,13);
    return 0;
}

  以下命令编译:

 mips-linux-gnu-gcc write_syscall.c -S -o write_syscall.s

  生成汇编代码如下所示:

    .file    1 "write_syscall.c"
    .section .mdebug.abi32
    .previous
    .nan    legacy
    .module    fp=xx
    .module    nooddspreg
    .abicalls
    .text
    .rdata
    .align    2
$LC0:
    .ascii    "Hello world!\012\000"
    .text
    .align    2
    .globl    main
    .set    nomips16
    .set    nomicromips
    .ent    main
    .type    main, @function
main:
    .frame    $fp,40,$31        # vars= 8, regs= 2/0, args= 16, gp= 8
    .mask    0xc0000000,-4
    .fmask    0x00000000,0
    .set    noreorder
    .set    nomacro
    addiu    $sp,$sp,-40
    sw    $31,36($sp)
    sw    $fp,32($sp)
    move    $fp,$sp
    lui    $28,%hi(__gnu_local_gp)
    addiu    $28,$28,%lo(__gnu_local_gp)
    .cprestore    16
    lui    $2,%hi($LC0)
    addiu    $2,$2,%lo($LC0)
    sw    $2,28($fp)
    li    $6,13            # 0xd   $a2寄存器
    lw    $5,28($fp)    # $a1 寄存器
    li    $4,1            # 0x1  $a0寄存器
    lw    $2,%call16(write)($28)  # $v0寄存器
    move    $25,$2  
    .reloc    1f,R_MIPS_JALR,write   
1:    jalr    $25
    nop

    lw    $28,16($fp)
    move    $2,$0
    move    $sp,$fp
    lw    $31,36($sp)
    lw    $fp,32($sp)
    addiu    $sp,$sp,40
    jr    $31
    nop

    .set    macro
    .set    reorder
    .end    main
    .size    main, .-main
    .ident    "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"

  简化之后,write.s的系统调用可以写为如下形式:

.section .text
.globl __start
.set noreorder
__start:
addiu $sp,$sp,-32
lui $t6,0x4142
ori $t6,$t6,0x430a
sw $t6,0($sp)
li $a0,1
addiu $a1,$sp,0
li $a2,5
li $v0,4004
syscall

  shell脚本编译为二进制可执行文件:

#!/bin/sh
# $ sh name.sh <source file><excute file>

src=$1
dst=$2
mips-linux-gnu-as $src -o s.o
mips-linux-gnu-ld s.o -o $dst
rm s.o

  readelf -S write定位text段入口地址:

pwndbg> disass /r 0x4000d0
Dump of assembler code for function _ftext:
   0x004000d0 <+0>:    27 bd ff e0    addiu    sp,sp,-32
   0x004000d4 <+4>:    3c 0e 41 42    lui    t6,0x4142
   0x004000d8 <+8>:    35 ce 43 0a    ori    t6,t6,0x430a
   0x004000dc <+12>:    af ae 00 00    sw    t6,0(sp)
   0x004000e0 <+16>:    24 04 00 01    li    a0,1
   0x004000e4 <+20>:    27 a5 00 00    addiu    a1,sp,0
   0x004000e8 <+24>:    24 06 00 05    li    a2,5
   0x004000ec <+28>:    24 02 0f a4    li    v0,4004
   0x004000f0 <+32>:    00 00 00 0c    syscall
   0x004000f4 <+36>:    00 00 00 00    nop
   0x004000f8 <+40>:    00 00 00 00    nop
   0x004000fc <+44>:    00 00 00 00    nop
End of assembler dump.

   可以提取16进制数来写shellcode。

qemu: uncaught target signal 4 (Illegal instruction) - core dumped

Illegal instruction (core dumped)

  在我们前面的write程序执行的时候,会出现这样两行报错。出现的原因是调用write系统调用之后,没有调用exit系统调用退出,继续执行了非法代码,下面写入exit系统调用来使程序正常退出:

.section .text
.globl __start
.set noreorder
__start:
addiu $sp,$sp,-32
lui $t6,0x4142
ori $t6,$t6,0x430a
sw $t6,0($sp)
li $a0,1
addiu $a1,$sp,0
li $a2,5
li $v0,4004
syscall
li $a0,1
li $v0,4001
li $a1,0
li $a2,0
syscall

  

 

 

 

 

 

 

 

 

 

posted @ 2020-11-20 21:41  Riv4ille  阅读(3858)  评论(0编辑  收藏  举报