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架構的區別:
- mips架構和x86架構中,棧的增長方向相同,都是從高地址向低地址增長,但是沒有棧底指針,所以調用一個函數是,需要將當前棧向低地址處移動n比特這個大小為n比特的空間就是此函數的棧楨存儲區域;
- mips架構中有葉子函數和非葉子函數的區別,葉子函數就是此函數自身不再調用別的函數,非葉子函數就是此函數自身調用別的函數。如果函數A調用函數B,調用者函數會在自己的棧頂預留一部分空間來保存被調用者(函數B)的參數,稱之為參數調用空間;
- 函數調用過程中,父函數調用子函數,復制當前$PC的值到$RA寄存器,然后跳轉到子函數執行。到子函數是,子函數如果為非葉子函數,則子函數的返回地址會先存入堆棧
- 參數傳遞方式,前四個參數通過$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